Unity5.6大规模地形资源创建方法

在很多仿真和游戏应用中都需要大规模地形,这样会使3D环境似乎“无限大”,增加用户的真实感,比如飞行模拟游戏。那么在Unity中如何实现大规模地形场景呢?官方中文论坛中有一个帖子讲得比较在点,帖子地址在这里。总结起来有三点:地形分块,动态加载和卸载,内存优化。其中前两点是最基本的。

最近一直在研究这个问题,也查了好多资料,博客、论坛,甚至一些科研论文,还并没有发现一套完整的解决方案(也许Asset Store上有,不过并没有找到免费的)。于是决定在这里将研究过程中的一些思路和实施方法分享给诸位Unity开发者,我会根据研究的进度不定时地更新这个系列。欢迎各位一起讨论!

我采用WorldMachine来绘制地形,并分块导出heightmap和texture。WorldMachine的使用暂时并没有深入研究,而分块导出的方法在WM手册上描述的比较详细。这一部分以后会单独更一篇,现在我们暂且不论。那么有了heightmap和texture如何创建Unity地形,或者说是Terrain Object呢?这里介绍两种方法:

1. 在Editor中手动创建

这部分很多博客都有介绍,简单介绍,有几个关键地方提一下。

创建Terrain对象,它自动包含一个Terrain组件,包含了所有属性设置。

点击这个Import Raw...按钮,导入你的高度图,我从WM中导出的heightmap资源采用.r16格式(16位raw),选择后弹出这个对话框:

Unity会自动识别heightmap的一些信息。我的平台选择Windows(mac有什么不同呢?)。Terrain Size设置包括地形大小和高度,注意这个Y值是你希望的地形高度中最大值和最小值之间的差值,它决定了你的地形的整体高度。我一开始对这个值的存在有一点疑惑,直到方法2的实现才明白其中的细节(先按下不表)。另外一个最重要的地方,一定要勾选Flip Vertically,为什么呢?WM三维空间中采用右手系(y轴向上),而Unity采用左手系(y轴也向上),在二者x轴正向相同的情况下,z轴恰好反转。如果没有勾选这个选项,你的texture显然就没法用了。设置完毕后,效果如下:

加上贴图也很简单,不过要将texture尺寸设置为和地形一样的大小,最终效果如下:

如此,一块地形就做好了,可以将它做成Prefab方便在场景中调用。不过这种方法的问题是,我的大地形可能要分许多块,那我总不能每一块都这样手工做一遍吧...不管工作量是否允许,面对这种重复工作我们都应该尽量去找到自动化实现的方法!因此就有了方法2。

2. 通过Editor扩展批量生产

不过说到底这个方法还是看的别人的,在Unity Community Wiki这个社区发现的一篇好帖,上链接。看完发现这个帖子是在Unity3版本时创建的,似乎现在这个社区都没有什么人光顾了。

Unity允许对Editor功能进行扩展,方法是在Editor文件夹下创建脚本,使用的API大多在UnityEditor命名空间中,这样我们可以用脚本代替一些手动工作。它的意义在于脚本和一些配置文档(比如txt,xml格式)作为静态的存在,可以保存我们的编辑指令、设置、操作等,方便完成对多个不同对象的重复操作,简单来说就是说就是“自动化”地“批量生产”。

现在,我们要编写Editor Script来实现方法1中的工作。那么首先我们要查询Script API文档,搞明白地形创建的流程。地形的核心在于TerrainData,每一次我们在hiearchy视图中创建一个Terrain对象时,Unity会自动为它创建一个TerrainData类型的对象,如图那个灰色的东西就是。

我们的heghtmap和texture都要交付于这个类型来实现。这里,我们先只介绍heightmap是如何加载进TerrainData对象的。先上代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using UnityEditor;
using System.IO;
using System;

public class ImportTerrain : MonoBehaviour
{
 [MenuItem("AssetDatabase/ImportTerrain")]
 static void GenerateTerrain()
 {
  string terrainDataPath = "Assets/512Test/td.asset"; //路径
  //创建TerrainData对象
  TerrainData terrainData = (TerrainData)AssetDatabase.LoadAssetAtPath(
   terrainDataPath, typeof(TerrainData));
  if (!terrainData)
  {
   terrainData = new TerrainData();
   AssetDatabase.CreateAsset(terrainData, terrainDataPath);
  }
  //地形设置
  Vector3 terrainSize = new Vector3(512, 250, 512); //地形的大小,最高、最低点的差值
  terrainData.size = terrainSize;
  terrainData.heightmapResolution = 513;
  float[,] height = new float[512, 512]; //地形高度数组
  //读取高度文件
  FileInfo hmFile = new FileInfo(@"Assets/512Test/output_x0_y0.r16");
  FileStream hmFs = hmFile.OpenRead();
  const int BYTESIZE = 512 * 512 * 2;
  byte[] hmB = new byte[BYTESIZE];
  int result = hmFs.Read(hmB, 0, BYTESIZE); //读取.r16字节流
  hmFs.Close();
  //赋值
  int i = 0;
  for (int x = 0; x < 512; x++)
  {
   for (int y = 0; y < 512; y++)
   {
    //注意两点:
    //Windows字节序
    //右手系转换到左手系
    height[511-x, y] = (hmB[i++] + hmB[i++] * 256.0f) / 65535.0f;
   }
  }
  terrainData.SetHeights(0, 0, height); //一切都是为了这个方法...
 }
}

这个脚本在Editor中添加了一个ImportTerrain按钮,点击这个按钮会自动创建一个名为hd的TerrainData资源,有了这个资源我们可以在场景中使用脚本很方便地创建Terrain对象。这个功能实现的重点在于:如何从.r16格式的高度图文件中提取我们需要的TerrainData.SetHeights方法的参数——一个二维float型数组。

首先,如何解析.r16文件?从WorldMachine中导出的.r16文件时一种二进制文件,存储了所有高度的字节,每个高度都有16位精度(而raw格式是8位)。比如从WM中导出一张分辨率为512*512的heightmap,格式存储为.r16,这样每个高度2byte,共512*512*2byte=512kb,查看一下.r16文件大小果然是这样。那么使用.NET平台的库函数可以很方便地将这些字节读取到内存中,存储为一个字节数组。

其次,如何解析这个字节数组?显然每两个相邻字节表示一个高度值,在Windows中字节序是低8位在前,高8位在后存储(iOS正好相反)。从帖子中的代码来看,.r16是按照行优先存储的,并且是以左上角为坐标原点。还记得我们使用Editor中Import Heightmap按钮时,弹出界面有一个Flipp y(翻转y轴)的选项吧?没错,我们的代码要考虑左手系与右手系的不同。最后一点,地形高度数组的元素全部是介于0~1之间的float型数值。实际高度值还由最大高度和最小高度之间的差值决定,这个值在terrainData.size中设置。我们可以发现这里的实现,与方法1中高度图对话框的设置之间具有明显的对应关系,也能够解释方法1中我们留下的一些疑问。

总结一下,这个脚本只是我参照原贴中javascript脚本,用C#写的测试脚本,存在着许多问题。比如,只实现了高度图的导入,并没有texture;代码可能比较丑陋可靠性完全没有考虑;并没有结合配置文件来实现所谓的“批量生产”...就当是一个初步研究成果吧,后续我会在项目中完善剩余的功能,编写完整的代码模块。也希望继续分享后续的Unity研究,欢迎各位开发者参与讨论!

参考文献(部分文中也有):

1. 飞行模拟大地形方案

2. 方法2的原文链接

3. Unity Script API文档

4. MSDN

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Unity3D绘制地形的实现方法

    项目中肯定会遇到需要用户自己绘制地形的需求,然后根据地形自动生成房间.下面说说我在绘制地形的实现方法. 我们百度可以看到很多关于自己创建mesh的博客,mesh的生成需要三角面顶点坐标以及顶点序列.所以,想要创建我们想要的mesh,首先要获取到绘制mesh的顶点.我们用户在绘制自己想创建的地形时会有很大的自由性.他是随心所欲想怎么画就怎么画.这也造就了很大的错误风险性,要求程序更加智能.好了,下面说下我们给自己程序设定的一些规则. 首先我们设置在绘制的时候摄像头的forward朝向Y轴向上,即我

  • Unity5.6大规模地形资源创建方法

    在很多仿真和游戏应用中都需要大规模地形,这样会使3D环境似乎"无限大",增加用户的真实感,比如飞行模拟游戏.那么在Unity中如何实现大规模地形场景呢?官方中文论坛中有一个帖子讲得比较在点,帖子地址在这里.总结起来有三点:地形分块,动态加载和卸载,内存优化.其中前两点是最基本的. 最近一直在研究这个问题,也查了好多资料,博客.论坛,甚至一些科研论文,还并没有发现一套完整的解决方案(也许Asset Store上有,不过并没有找到免费的).于是决定在这里将研究过程中的一些思路和实施方法分享

  • Mybatis 创建方法、全局配置教程详解

    总体介绍:MyBatis实际上是Ibatis3.0版本以后的持久化层框架[也就是和数据库打交道的框架]! 和数据库打交道的技术有: 原生的JDBC技术--->Spring的JdbcTemplate技术 这些工具都是提供简单的SQL语句的执行,但是和我们这里学的MyBatis框架还有些不同, 框架是一整套的东西,例如事务控制,查询缓存,字段映射等等. 我们用原生JDBC操作数据库的时候都会经过: 编写sql---->预编译---->设置参数----->执行sql------->

  • Android编程之数据库的创建方法详解

    本文实例讲述了Android编程之数据库的创建方法.分享给大家供大家参考,具体如下: 主java package com.itheima.createdatabase; import android.app.Activity; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; public class MainActivity exten

  • vue-cli与webpack处理静态资源的方法及webpack打包的坑

    通过Vue-cli进行webpack打包的坑 Vue-cli为Vue项目搭建的脚手架的确很方便,但打包时容易出现空白页,或者对应的静态资源加载不了. 我是通过将项目/config下的index.js的assetsPublicPath变成'./',变成相对路径,进行解决. cd vue demo npm run dev //运行程序 npm run bulid //webpack打包 处理静态资源 你也许会注意到vue-cli与webpack结合的项目中,我们通常会有两个静态资源的路径:src/a

  • 基于Springboot2.3访问本地路径下静态资源的方法(解决报错:Not allowed to load local resource)

    最近在做的一个项目中有一个比较奇葩的需求: 要在springboot中,上传本地的图片进行展示 我的第一反应是,直接在数据库字段加一个存储本地路径的字段,然后用thymeleaf的th:src渲染到前端就好了嘛! 理想很丰满,但现实却很骨感~ 前端报了这样的错误Not allowed to load local resource 于是我想到了可以使用IO将图片先上传到static/images目录下,这样就不会出现禁止访问本地路径的问题了 但是这样实现,问题又来了:上传后的图片必须重启sprin

  • jquery-mobile表单的创建方法详解

    本文实例讲述了jquery-mobile表单的创建方法.分享给大家供大家参考,具体如下: 一.注意事项 1. <form> 元素必须设置 method 和 action 属性 2. 每个表单元素必须设置唯一的 "id" 属性. 该 id 在站点的页面中必须是唯一的. 这是因为 jQuery Mobile 的单页面导航模型允许许多不同的"页面"同时呈现. 3. 每个表单元素必须有一个标记(label). 请设置 label 的 for 属性来匹配元素的 i

  • Java 读取外部资源的方法详解及实例代码

    Java 读取外部资源的方法详解 在Java代码中经常有读取外部资源的要求:如配置文件等等,通常会把配置文件放在classpath下或者在web项目中放在web-inf下. 1.从当前的工作目录中读取: try { BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("wkdir.txt"))); String str; while ((str = in.readLine())

  • java实现HttpClient异步请求资源的方法

    本文实例讲述了java实现HttpClient异步请求资源的方法.分享给大家供大家参考.具体实现方法如下: package demo; import java.util.concurrent.CountDownLatch; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.nio.client.DefaultHttpAsyn

  • 详解iOS应用中自定义UIBarButtonItem导航按钮的创建方法

    iOS系统导航栏中有leftBarButtonItem和rightBarButtonItem,我们可以根据自己的需求来自定义这两个UIBarButtonItem. 四种创建方法 系统提供了四种创建的方法: 复制代码 代码如下: - (instancetype)initWithBarButtonSystemItem:(UIBarButtonSystemItem)systemItem target:(id)target action:(SEL)action; - (instancetype)init

随机推荐