CSharpGL(5)解析3DS文件并用CSharpGL渲染
我曾经写过一个简单的*.3ds文件的解析器,但是只能解析最基本的顶点、索引信息,且此解析器是仿照别人的C++代码改写的,设计的也不好,不足方便扩展。
现在我重新设计实现了一个*.3ds文件的解析器,它能解析的Chunk类型更多,且容易扩展。以后需要解析更多类型的Chunk时比较简单。
下载
这个3DS解析器是CSharpGL的一部分,CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)
本文所用的3ds文件您可以在此(http://www.cgrealm.org/d/downpage.php?n=2&id=15764::1326768548)下载,由于文件比较大我就不上传了。
3DS文件格式
树
3ds文件是二进制的。3ds格式的基本单元叫块(chunk)。我们就是读这样一块一块的信息。目录树如下,缩进风格体现了块的父子关系。可见3ds模型文件和XML文件类似,都是只有1个根结点的树状结构。
2 ├─ 0x0002 // M3D Version
3 ├─ 0x3D3D // 3D Editor Chunk
4 │ ├─ 0x4000 // Object Block
5 │ │ ├─ 0x4100 // Triangular Mesh
6 │ │ │ ├─ 0x4110 // Vertices List
7 │ │ │ ├─ 0x4120 // Faces Description
8 │ │ │ │ ├─ 0x4130 // Faces Material
9 │ │ │ │ └─ 0x4150 // Smoothing Group List
10 │ │ │ ├─ 0x4140 // Mapping Coordinates List
11 │ │ │ └─ 0x4160 // Local Coordinates System
12 │ │ ├─ 0x4600 // Light
13 │ │ │ └─ 0x4610 // Spotlight
14 │ │ └─ 0x4700 // Camera
15 │ └─ 0xAFFF // Material Block
16 │ ├─ 0xA000 // Material Name
17 │ ├─ 0xA010 // Ambient Color
18 │ ├─ 0xA020 // Diffuse Color
19 │ ├─ 0xA030 // Specular Color
20 │ ├─ 0xA200 // Texture Map 1
21 │ ├─ 0xA230 // Bump Map
22 │ └─ 0xA220 // Reflection Map
23 │ │ // Sub Chunks For Each Map
24 │ ├─ 0xA300 // Mapping Filename
25 │ └─ 0xA351 // Mapping Parameters
26 └─ 0xB000 // Keyframer Chunk
27 ├─ 0xB002 // Mesh Information Block
28 ├─ 0xB007 // Spot Light Information Block
29 └─ 0xB008 // Frames (Start and End)
30 ├─ 0xB010 // Object Name
31 ├─ 0xB013 // Object Pivot Point
32 ├─ 0xB020 // Position Track
33 ├─ 0xB021 // Rotation Track
34 ├─ 0xB022 // Scale Track
35 └─ 0xB030 // Hierarchy Position
实际上完整的chunk列表有上千种类型,我们只需解析其中的顶点列表、面列表和纹理UV列表就行了。
以类型标识为0x4D4D的MAIN CHUNK为例,整个3ds文件的前两个byte必须是0x4D4D,否则就说明这个文件不是3ds模型文件。然后从第3到第6个byte是一个Uint32型的数值,表示整个MAIN CHUNK的长度。由于MAIN CHUNK是整个3ds文件的根结点,它的长度也即整个3ds文件的长度。
块(Chunk)的结构
每一个“chunk”的结构如下所示:
偏移量 | 长度 |
|
0 | 2 | 块标识符 |
2 | 4 | 块长: 块数据 + 子块内容 |
6 | n | 块数据 |
6+n | m | S子块 |
文件内容
一个3DS文件,其中包含若干材质对象,材质对象里有材质参数和贴图文件名;还有若干子模型,每个子模型都由顶点位置、UV位置、三角形索引和分组索引构成。分组索引是这么一个东西:它由若干三角形索引的编号和一个材质对象名组成。这个分组索引似乎暗示着:渲染过程应根据分组索引描绘的顺序进行,即取出一个分组索引,绑定它指定的材质和贴图,渲染它指定的三角形,然后取出下一个分组索引继续上述渲染操作。我们将在后文进行验证。
解析器设计思路
在【轻松一刻】实战项目开发(二) list数据去重 数据追加与缓存 - sphere 阅读原文»
引入开源控件 PullToRefresh 下拉刷新列表
每次下拉刷新都会发送请求,从接口返回json信息。
如果前后两次请求返回的数据中有重复的数据 该怎么给list去重
在上一篇中我们重写了实体Data的hashcode和equals方法
* 因为更新时间和unixtime都不是唯一的
* 这里使用唯一标识hashId来得到哈希码
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((hashId == null) ? 0 : hashId.hashCode());
return result;
}
/**
* 因为更新时间和unixtime都不是唯一的
* 这里使用唯一标识hashId来比较
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Data other = (Data) obj;
if (hashId == null) {
if (other.hashId != null)
return false;
} else if (!hashId.equals(other.hashId))
return false;
return true;
}
因为data数据中只有hashId 是唯一标识,所以我们使用它做比较
使用set中不能添加重新元素的特性作为判断条件去重
* 重写Data中的hashCode和equals方法
* 使用set中不能添加重新元素的特性作为判断条件
* 将不重复的data元素依次放入临时的newlist
* 循环完毕后,将原始list清空,addAll(newlist)
* @param list
* @return
*/
public static List<Data> removeDuplicateDataInOrder(List<Data> list)
{
HashSet<Data> hashSet = new HashSet<Data>();
List<Data> newlist = new ArrayList<Data>();
for (Iterator iterator = list.iterator(); iterator.hasNext();)
{
Data element = (Data) iterator.next();
if (hashSet.add(element))
{
newlist.add(element);
}
}
list.clear();
list.addAll(newlist);
return list;
}
而为了保证数据排列的先后顺序 我们应该在去重复之前做如下操作
1.如果是下拉刷新 在把数据添加到list的头部 list.addAll(0,list<T>)
2.如果是上拉加载更多则把数据加载到尾部 list.addAll(list<T>)
聚合数据接口 还可以按时间戳返回该时间点前或后的笑话列表
请求示例:http://japi.juhe.cn/joke/content/list.from?key=您申请的KEY&page=2&pagesize=10&sort=asc&time=1418745237
其中参数中的sort param sort desc:指定时间之前发布的,asc:指定时间之后发布的 ,time 表示时间戳
所以我们在上拉加载更多时 使用这种请求方式,以当前list中最后一条数据的时间戳为节点 返回该时间点前笑话列表。
故上拉加载更多时,list没有必要再去重复了、
// 将新刷新的数据 添加到数据集头部
mCurrentListItems.addAll(0,result.getResult());
// 去除list中重复的数据
mCurrentListItems = UtilsHelper.removeDuplicateDataInOrder(mCurrentListItems);
}else if(flag == PULL_UP_REFRESH_FLAG){
// 将新加载的数据 添加到数据集尾部
// 因为是以最后一条数据的时间戳向前查询,故不存在重复,无需list去重
mCurrentListItems.addAll(result.getResult());
pullUpPageNumber++;
}
如何得到本次请求之后更新了多少条数据呢,很简单只要保存上次的list,并与最新的list的size做对比即可。
使用handler处理,并在主线程更新UI
int count = mCurrentListItems.size() - mLastListItems.size();
android.os.Message msg = new android.os.Message();
msg.what = UPDATE_DATA_COUNT_MESSAGE;
msg.arg1 = count;
mHandler.sendMessage(msg);
}
然后通知adapter数据集发生改变,并调用listview (listview是PullToRefreshListView的实例) 的onRefreshComplete()方法
mAdapter.notifyDataSetChanged();
mListView.onRefreshComplete();
假设我们处于没有网络的环境,那就有必要搞一个缓存了,以便离线查看,最好是缓存上次我们退出应用时显示的list数据。
没有使用sqlite,暂时使用文件,缓存到sdcard的一个目录中(/storage/emulated/0/qingsongyike/cache)
boolean sdCardExist = Environment.getExternalStorageState()
.equals(android.os.Environment.MEDIA_MOUNTED); //判断sd卡是否存在
if(sdCardExist){
Log.d("UtilsHelper", "sdcard exist.");
File root = Environment.getExternalStorageDirectory();
String path = root.getPath()+File.separator+"qingsongyike"+File.separator+"cache"阅读更多内容
没有评论:
发表评论