Unity实战篇,小岛城堡里的常春藤,听新发剖析Unity案例知识点

Posted 林新发

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity实战篇,小岛城堡里的常春藤,听新发剖析Unity案例知识点相关的知识,希望对你有一定的参考价值。

一、前言

嗨,大家好,我是新发。
记得初中的时候语文课本里有一篇名字叫《最后一片叶子》的文章,作者是著名的短篇小说家欧·亨利。

《最后一片叶子》又叫《最后一片常春藤叶》,文中讲述了老画家贝尔曼为了鼓励贫病交加的青年画家顽强地活下去,在风雨之夜挣扎着往墙上画了一片永不凋零的常春藤叶。他为此用生命绘制的杰作付出了生命的代价,但青年画家却因此获得勇气而活了下来的故事。

常春藤寓意着希望、朝气蓬勃,也象征着忠诚,还代表着和不朽的青春,因为它一年四季都是常绿的,所以很多人喜欢在阳台上种常春藤,显得格外有生气。本文,我就来证明一下常春藤的魔力,我将在Unity中演示制作常春藤的过程,并讲解其中涉及到的一些技术知识点。

二、效果演示

场景,我希望是在一个优美的岛屿城堡,我找到了一个不错的场景,如下:

注:喜欢这个城堡场景资源的同学,可以自行从这里下载:https://assetstore.unity.com/packages/3d/props/exterior/low-poly-brick-houses-131899

在这里插入图片描述
在窗户上绘制生成常春藤,如下:
在这里插入图片描述
对比下前后效果,是不是有了常春藤之后,瞬间生机盎然了~
在这里插入图片描述

三、常春藤生成器工具下载

我使用的是Github的这个工具:hedera,这个工具可以很方便地在场景中制作并生成常春藤一样的植物,感兴趣的同学可以下载从GitHub上下载来学习。
GitHub地址:https://github.com/radiatoryang/hedera
在这里插入图片描述

四、 工具使用

1、创建根节点:lvy GameObject

点击菜单Hedera / Create / Create New lvy GameObject
在这里插入图片描述
此时会生成一个lvy Group节点,它身上会带一个lvyBehavior组件,我们下面生成的常春藤就是在这个节点之下生成的。
在这里插入图片描述

2、创建配置文件:lvy Profile Asset

上面我们可以看到,lvyBehavior组件需要指定一个配置文件,这个配置文件用于配置常春藤生成的规则与相关参数。
工具已经帮我们做好了几个配置,在Runtime/lvyProfiles目录中,
在这里插入图片描述
为了演示,我创建一个新的,点击Create new lvy Profile Asset...按钮,
在这里插入图片描述
将其保存到Runtime/lvyProfiles目录中,
在这里插入图片描述
生成后选中它,可以在Inspector视图中看到配置的参数,
在这里插入图片描述
参数说明:

参数说明
Length生成长度,可以设置上下限,从这个范围内进行随机
Branch Chance %生成分支的概率
Random Spread %随机分布率
Branch Thickness根茎的粗度
Leaf Size Radius叶子大小
Leaf Density %叶子密度
Leaf Colors叶子颜色
Brahcn Material根茎的材质
Leaf Material叶子的材质

3、贴图与材质球

我们需要先准备常春藤的贴图(包括根茎+叶子),例:
在这里插入图片描述
制作根茎和叶子的材质球:
在这里插入图片描述
材质球设置如下(根茎+叶子):
在这里插入图片描述 在这里插入图片描述
lvy Profile Asset设置根茎和叶子的材质球,
在这里插入图片描述

4、绘制常春藤

选中lvy Group,点击Start Painting lvy按钮,
在这里插入图片描述
然后把鼠标移到Scene视图中,即可看到有个蓝紫色的圈圈投射在物体表面上,
在这里插入图片描述
此时按住鼠标滑动即可生成常春藤,
在这里插入图片描述

5、修改叶子颜色

我们看到绘制出来的叶子颜色是 白/绿/黄 的,
在这里插入图片描述
这是因为在lvy Profile Asset中设置的叶子颜色是这样的:
在这里插入图片描述
我们可以将其修改成我们想要的其他颜色,比如改成这样:
在这里插入图片描述
重新绘制出来的叶子颜色如下:
在这里插入图片描述

6、修改叶子大小

调整Leaf Size Radius可以修改叶子的大小,
在这里插入图片描述
我们把叶子大小调小,调整参数后可以点击Re-mesh Visible按钮,就会根据调整后的参数重新运算~
在这里插入图片描述
调整前是这样:
在这里插入图片描述
调整后是这样:
在这里插入图片描述

7、修改叶子密度

调整Leaf Density %可以修改叶子密度,
在这里插入图片描述
我们把叶子密度调大,如下:
在这里插入图片描述

8、修改根茎粗细

我们觉得根茎有点粗,
在这里插入图片描述
想调细一点,调整Branch Thickness,把根茎调细,
在这里插入图片描述
如下:
在这里插入图片描述

9、修改生长长度

调整Length可以修改生长长度,
在这里插入图片描述
我们测试下最小值和最大值的效果,调整为最小值,此时绘制常春藤不会自动继续生长,

在这里插入图片描述
效果如下:
在这里插入图片描述
现在,我们把Lehgth调为最大值,
在这里插入图片描述
因为它生长力太强了,所以我在地面上演示,感受一下,
在这里插入图片描述

10、修改分支概率

我们看到生长过程中的分支概率比较低,我们可以调整Branch Chance %来修改分支概率,
在这里插入图片描述
我们把分支概率调到最大值,感受一下,
在这里插入图片描述

11、删除已绘制的常春藤

假设我们要删除这条常春藤,并不是直接delete它的GameObject
在这里插入图片描述
而是先选中它所在的Group
在这里插入图片描述
然后点击对应的垃圾桶按钮,
在这里插入图片描述
如果一个Group下有多条常春藤,则会会显示多个item
在这里插入图片描述

五、拓展知识解答

1、常春藤的Mesh资源存放在哪里

我们点开常春藤的节点,可以看到一个根茎节点和一个叶子节点,
在这里插入图片描述
工具帮我们生成了根茎和叶子的Mesh,这些Mesh文件会自动存放在场景所在目录的同名目录中,
在这里插入图片描述
它是序列化在一个.assset文件中的,
在这里插入图片描述
根茎的Mesh
在这里插入图片描述

叶子的Mesh
在这里插入图片描述
思考:它是如何将网格资源序列化到asset文件中的?
解答
需要序列化的类继承ScriptableObject,例:

public class IvyDataAsset : ScriptableObject
{
	//...
}

通过ScriptableObject.CreateInstance创建序列化文件,例:

public static IvyDataAsset CreateNewDataAsset(string mainFolder, string sceneName, string path) 
{
	if ( !AssetDatabase.IsValidFolder(path) ) {
		var folderGUID = AssetDatabase.CreateFolder( mainFolder, sceneName );
		path = AssetDatabase.GUIDToAssetPath(folderGUID);
	}
	
	IvyDataAsset asset = ScriptableObject.CreateInstance<IvyDataAsset>();
	AssetDatabase.CreateAsset(asset, path + "/HederaData.asset");
	AssetDatabase.SaveAssets();
	return asset;
}

然后在IvyDataAsset中定义需要序列化的内容,

// IvyDataAsset.cs

public class IvyDataAsset : ScriptableObject
{
	public IvyDictionary meshList = new IvyDictionary();
	
	[System.Serializable]
    public class IvyDictionary : SerializableDictionary<long, Mesh> { }
	
	[System.Serializable]
    public class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, ISerializationCallbackReceiver
    {
        [SerializeField]
        private List<TKey> keys = new List<TKey>();
        
        [SerializeField]
        private List<TValue> values = new List<TValue>();
	
		// ...
	}
}

关于Unity的类的序列化,还可以参见我早先写的这篇文章:《unity 类的序列化》

如果我们想要自定义Inspector界面的内容,我们还可以写对应的Editor类,重写OnInspectorGUI函数,例:

// IvyDataAssetEditor.cs

[CustomEditor( typeof(IvyDataAsset))]
public class IvyDataAssetEditor : Editor
{
	public override void OnInspectorGUI() 
	{
		var data = (IvyDataAsset)target;
		// ...
	}
}

2、如何删除无用的Mesh

选中HederaData文件,点击Cleanup Unreferenced Meshes按钮,接口自动清理无用的Mesh资源。
在这里插入图片描述
对应的逻辑:

// IvyDataAssetEditor.cs

if ( GUILayout.Button(content) ) {
   var allReferencedMeshes = data.meshList.Values.ToList();
   for ( int i=0; i<allSubassets.Length; i++ ) {
       if (!allReferencedMeshes.Contains((Mesh)allSubassets[i])) {
           Object.DestroyImmediate(allSubassets[i], true);
       }
   }
   EditorUtility.SetDirty(data);
   AssetDatabase.SaveAssets();
}

3、如何获取鼠标投射到物体表面的位置

上面我们看到,鼠标移到Scene视图中,可以看到有个蓝紫色的圈圈投射在物体表面上,这个是如何实现的呢~
在这里插入图片描述
用的是射线检测接口:

public static bool Raycast(
		Vector3 origin, 
		Vector3 direction, 
		out RaycastHit hitInfo, 
		float maxDistance, 
		int layerMask, 
		QueryTriggerInteraction queryTriggerInteraction);

例:

// IvyEditor.cs

public void MousePosition ()
{
	// 射线
    Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
    RaycastHit hit;
	// 射线检测
    if (Physics.Raycast(ray.origin, 
		ray.direction, 
		out hit, 
		Mathf.Infinity, 
		ivyBehavior.profileAsset.ivyProfile.collisionMask, 
		QueryTriggerInteraction.Ignore)) 
    {
        mousePos = hit.point + hit.normal * 0.05f;
        mouseNormal = hit.normal;
        Handles.color = Color.blue;
        // 绘制线圈
        DrawThiccDisc(mousePos, hit.normal, Mathf.Max(0.1f, ivyBehavior.profileAsset.ivyProfile.ivyStepDistance));
        // 绘制法线
        Handles.DrawLine(mousePos, mousePos + hit.normal * 0.25f);
    }
}

// 绘制线圈
void DrawThiccDisc(Vector3 mousePos, Vector3 normal, float radius) 
{
    var originalColor = Handles.color;
    Handles.color = new Color( originalColor.r, originalColor.g, originalColor.b, 0.4f);
    Handles.DrawSolidDisc( mousePos, normal, radius);
    Handles.color = originalColor;
    Handles.DrawWireDisc(mousePos, normal, radius - 0.01f );
    Handles.DrawWireDisc(mousePos, normal, radius );
    Handles.DrawWireDisc(mousePos, normal, radius + 0.01f );
}

注:我之前写了一篇文章,《使用Unity ShaderGraph实现在模型上涂鸦的效果,那么,纹个手吧》,里面也用到了射线检测,感兴趣的同学可以打开阅读以下~

4、场景中的水面是怎么做的(半透明和反射效果)

在这里插入图片描述

先创建一个空白的平面(Plane)作为水底,
在这里插入图片描述
再创建一个平面作为水面,
在这里插入图片描述
为水面创建一个材质球,
在这里插入图片描述
材质球设置为半透明模式(Transparent),设置一下颜色,提高光滑度(Smoothness),
在这里插入图片描述
这样,水面就具有半透明效果,同时也可以反射天空的影像,我们创建个Cube测试一下,
在这里插入图片描述

5、场景中的天空是怎么做的

在这里插入图片描述
场景中的天空是用天空盒做的,我们需要先准备720度天空全景图,可以把自己想象成坐在一个正方体的内部,这六张图就是对应正方体里面的6个面(前后左右上下),
在这里插入图片描述
创建一个材质球,shader使用Skybox/6 Sided,然后设置前后左右上下6个面的贴图,
在这里插入图片描述
最后,把材质球拖到Scene视图空白处即可,或者点击菜单Window / Rendering / Lighting
在这里插入图片描述
点击Environment标签页,设置天空盒材质球即可,
在这里插入图片描述
我上一篇文章《[原创] 用Unity等比例制作广州地铁,广州加油,早日战胜疫情(Unity | 地铁地图 | 第三人称视角)》里面也用到了天空盒:

更多免费天空盒资源下载:
https://assetstore.unity.com/packages/2d/textures-materials/sky/fantasy-skybox-free-18353
https://assetstore.unity.com/packages/2d/textures-materials/sky/8k-skybox-pack-free-150926
https://assetstore.unity.com/packages/2d/textures-materials/sky/customizable-skybox-174576
https://assetstore.unity.com/packages/vfx/shaders/free-skybox-extended-shader-107400
https://assetstore.unity.com/packages/2d/textures-materials/sky/farland-skies-cloudy-crown-60004

6、场景中的草是怎么做的,为什么它会摇啊摇

在这里插入图片描述
场景中的草一摇一摇的,这是怎么做的呢~
这里用到了Unity的地形编辑器Terrain,草是地形的一部分,我们把环境中其他物体隐藏起来,只留下地形,如下:
在这里插入图片描述
所以我们需要先创建一个地形,在Hierarchy视图空白处右键鼠标,点击菜单3D Object / Terrain
在这里插入图片描述
此时就可以创建一个地形物体,
在这里插入图片描述
它身上会带一个Terrain组件,我们点击Place Details按钮(第四个按钮,我圈出来那个),我们就可以在地形上刷出细节物体,通常用来做草。
在这里插入图片描述
不过这个时候我们什么也刷不出来,这是因为我们还没有设置笔刷图片,
在这里插入图片描述
我们先准备一张草的图片,如下:
在这里插入图片描述
点击Edit Details...按钮,
在这里插入图片描述
点击Add Grass Texture按钮,
在这里插入图片描述
设置Detail Texture````为刚刚的草的图片,点击Add```按钮,
在这里插入图片描述
其他参数说明:

参数说明
Detail Texture选择一张花或者草的贴图
Min Width、Max Width、Min Height、Max Height单个草物体的最大最小的宽高值
Noise Spread添加一点分布随机度
Healthy Color草的健康颜色(会被tint到贴图的上部)
Dry Color草的干枯颜色(会被tint到贴图的底部)
Billboard是否以永远面向摄像机的单面形式生成单个草物体,如果选否的话就会以十字交叉双平面方式来生成单个草物体

这样草的笔刷图片就制作好了,
在这里插入图片描述
这样就可以刷出草地啦~
在这里插入图片描述

如果想要删除草地,只需要按住Ctrl键不放,鼠标一刷,就可以把对应的草地删除了~
在这里插入图片描述
这些草之所以会摇啊摇,是因为有风,Terrain组件的设置里可以设置风的大小等参数,如下:
在这里插入图片描述
细心的朋友应该注意到了,这个草堆并不是地形刷出来的,为什么它也会摇啊摇,
在这里插入图片描述
这个是通过shader的顶点着色器来控制的,例:

// FoliageShader.shader

float random(float3 p)
{
	return frac(43758.5453 * sin(dot(p, float3(12.9898, 78.233, 45.5432)) % 3.14159));
}

// 顶点着色器
void vert(inout appdata_full v)
{
	// 随机偏移
	float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
	float3 offset = _Intensity * (sin(worldPos.xyz + _Time.y * _WindSpeed) + _Randomness * random(worldPos));
	// 设置顶点坐标偏移
	v.vertex.xyz += offset;
}

六、结束语

好了,就先写这么多吧,如果有什么疑问,欢迎留言或私信~
最后,晒下我小屋的常春藤~
在这里插入图片描述

以上是关于Unity实战篇,小岛城堡里的常春藤,听新发剖析Unity案例知识点的主要内容,如果未能解决你的问题,请参考以下文章

浅墨Unity3D Shader编程之六 暗黑城堡篇: 表面着色器(Surface Shader)的写法

Unity3D Shader编程之三 光之城堡篇:子着色器通道与标签的写法 & 纹理混合

浅墨Unity3D Shader编程之三 光之城堡篇:子着色器通道与标签的写法 &amp; 纹理混合

Python成长笔记 - 基础篇 (十四)--堡垒机

洛谷 P2683 小岛

比较温莎城堡、Unity 和 StructureMap