八、三维模型查看器
三维模型无处不在,从机械零件的机械工程到医学影像;从电子游戏设计到 3D 打印。3D 模型像照片、视频、音乐和其他媒体一样丰富。然而,虽然浏览器和应用对其他媒体类型有本地支持,但 3D 模型却没有这么多。总有一天 3D 观看标准会集成到浏览器中(比如 WebGL 和 WebVR)。在那之前,我们将不得不依靠插件和姐妹应用来查看我们的模型。例如,在网上可以找到 OBJ 格式的免费 3D 文件模型,包括 TF3DM(http://tf3dm.com/)、TurboSquid(http://www.turbosquid.com/)和其他许多模型(http://www . hongkiat . com/blog/60-卓越-免费-3D-model-网站/ )。
在这个项目中,我们将构建一个安卓 3D 模型查看器应用,让您使用纸板虚拟现实耳机以 3D 方式打开和查看模型。我们将使用的文件格式是 OBJ,这是一种由波前技术公司首先为电影 3D 动画开发的开放格式。OBJs 可以由许多 3D 设计应用创建和导出,包括开源应用,如 Blender 和 MeshLab,以及商业应用,如 3D Studio Max 和 Maya。OBJ 是一个非压缩的纯文本文件,它存储由三角形(或更高度数的多边形)组成的三维对象的表面网格的描述。
为了实现查看器,我们将读取和解析 OBJ 文件模型,并以 3D 形式显示它们,以便用纸板进行查看。我们将通过执行以下步骤来实现这一点:
- 设置新项目
- 编写 OBJ 文件解析器来导入几何图形
- 显示三维模型
- 使用用户的头部运动旋转对象的视图
这个项目的源代码可以在 Packt Publishing 网站上找到,也可以在 GitHub 上的https://github.com/cardbookvr/modelviewer找到(每个主题作为一个单独的提交)。
建立新项目
为了构建这个项目,我们将使用在第 5 章、RenderBox 引擎中创建的RenderBox
库。您可以使用您的,或者从本书提供的可下载文件或我们的 GitHub 存储库中获取一份副本(使用提交标记的after-ch7
—https://GitHub . com/cardbookr/renderboxlib/releases/tag/after-ch7)。关于如何导入RenderBox
库的更详细描述,请参考第 5 章、RenderBox 引擎的最后一节在未来项目中使用 RenderBox。要创建新项目,请执行以下步骤:
- Android Studio 打开后,创建一个新项目。让我们将其命名为
Gallery360
,并以空活动为目标安卓 4.4 KitKat (API 19) 。 - 使用文件 | 新模块 | 导入,为
renderbox
、common
和core
包创建新模块。JAR/。AAR 包装。 - 使用文件 | 项目结构,将模块设置为应用的依赖项。
- 按照第 2 章、框架纸板项目中的说明编辑
build.gradle
文件,根据 SDK 22 进行编译。 - 更新
/res/layout/activity_main.xml
和AndroidManifest.xml
,如前几章所述。 - 将
MainActivity
编辑为class MainActivity extends CardboardActivity implements IRenderBox
,实现接口方法存根( Ctrl + I )。
我们可以在MainActivity
中定义onCreate
方法。该类现在具有以下代码:
public class MainActivity extends CardboardActivity implements IRenderBox {
private static final String TAG = "ModelViewer";
CardboardView cardboardView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
cardboardView = (CardboardView) findViewById(R.id.cardboard_view);
cardboardView.setRenderer(new RenderBox(this, this));
setCardboardView(cardboardView);
}
@Override
public void setup() {
}
@Override
public void preDraw() {
// code run beginning each frame
}
@Override
public void postDraw() {
// code run end of each frame
}
}
您可以暂时在场景中添加一个立方体,以帮助确保一切设置正确。将其添加到setup
方法中,如下:
public void setup() {
new Transform()
.setLocalPosition(0,0,-7)
.setLocalRotation(45,60,0)
.addComponent(new Cube(true));
}
如果你记得的话,一个Cube
就是一个Component
,加到一个Transform
上。Cube
定义其几何形状(例如顶点)。Transform
定义其在三维空间中的位置、旋转和缩放。
您应该能够在没有编译错误的情况下点击运行‘应用’,并在您的安卓设备上看到立方体和纸板分屏视图。
了解 OBJ 文件格式
本项目的目标是以波前 OBJ 格式查看三维模型。在开始编码之前,我们先来看看文件格式。可以在http://www.fileformat.info/format/wavefrontobj/egff.htm找到参考。
众所周知,三维模型可以表示为一个由 X、Y 和 Z 顶点组成的网格。顶点集被连接以定义网格表面的一个面。全网格曲面是这些面的集合。
还可以为每个顶点指定一个法向量和/或纹理坐标。法线向量定义了顶点处的朝外方向,用于照明计算。紫外线纹理坐标可用于将纹理图像映射到网格表面。该格式还有其他功能,包括自由形式的曲线和材质,我们在本项目中不支持。
作为纯文本文件,OBJ 被组织为单独的文本行。每一个非空行都以一个关键字和该关键字的数据开始,用空格分隔。注释以#
开头,被解析器忽略。
OBJ 数据关键词包括:
v
:几何顶点(例如v 0.0 1.0 0.0
)vt
:纹理顶点(例如vt 0.0 1.0 0.0
)【我们的项目不支持】vn
:顶点法线(例如vn 0.0 1.0 0.0
)f
:多边形面索引(例如f 1 2 3
)
表面值是指向顶点列表中顶点的索引(第一个从 1 开始)。
至于指定面索引的f
命令,它们是索引到顶点列表中的整数值。当有三个指数时,它描述一个三角形;四描述一个四边形,以此类推。
当纹理顶点存在时,它们被引用为斜线后的第二个数字,例如f 1/1 2/2 3/3
。我们现在不支持它们,但是我们可能需要在f
命令中解析它们。当顶点法线存在时,它们被引用为斜线后的第三个数字,例如f 1//1 2//2 3//3
或f 1/1/1 2/2/2 3/3/3
。
索引可以是负数,在这种情况下,它们将最后一个(最近遇到的)项目引用为-1,将前一个引用为-2,依此类推。
其他行,包括我们这里不支持的数据,将被忽略。
例如,以下数据表示一个简单的三角形:
# Simple Wavefront file
v 0.0 0.0 0.0
v 0.0 1.0 0.0
v 1.0 0.0 0.0
f 1 2 3
我们在 OBJ 的实施是有限的。它可以安全地处理本书中包含的示例模型,以及您可能会在互联网上找到或自己制作的其他模型。但是,这是一个示例代码和演示项目。编写一个健壮的数据导入器并在我们的RenderBox
引擎中支持 OBJ 的许多特性超出了本书的范围。
创建模型对象类
首先,我们将定义一个扩展RenderObject
的ModelObject
类。它将从 OBJ 文件加载模型数据,并设置其材质所需的缓冲区(以及要在虚拟现实场景中渲染的 OpenGL ES 着色器)。
右键点击app/java/com.cardboardvr.modelviewer/
文件夹,转到新建 | Java 类,命名为ModelObject
。将其定义为extends RenderObject
,如下所示:
public class ModelObject extends RenderObject {
}
就像我们在前面几章中所做的一样,当引入新的RenderObjects
时,我们将有一个或多个构造函数可以实例化一个Material
并设置缓冲区。对于ModelObject
,我们将传入一个文件资源句柄,解析该文件(参考下一个主题),并创建一个纯色材质(最初,没有灯光),如下所示:
public ModelObject(int objFile) {
super();
InputStream inputStream = RenderBox.instance.mainActivity.getResources().openRawResource(objFile);
if (inputStream == null)
return; // error
parseObj(inputStream);
createMaterial();
}
现在添加如下材质。首先,为缓冲区声明变量(就像我们在前面的项目中为其他RenderObjects
所做的那样)。这些可以是私有的,但如果我们想在外部定义新材质,我们的惯例是保持它们的公共性:
public static FloatBuffer vertexBuffer;
public static FloatBuffer colorBuffer;
public static FloatBuffer normalBuffer;
public static ShortBuffer indexBuffer;
public int numIndices;
下面是createMaterial
方法(从构造函数调用):
public ModelObject createMaterial(){
SolidColorLightingMaterial scm = new SolidColorLightingMaterial(new float[]{0.5f, 0.5f, 0.5f, 1});
scm.setBuffers(vertexBuffer, normalBuffer, indexBuffer, numIndices);
material = scm;
return this;
}
接下来,我们实现parseObj
方法。
解析 OBJ 模型
parseObj
方法将资源文件作为InputStream
打开。它一次读取一行,解析命令和数据,构建模型的顶点、法线和索引列表。然后,我们从数据中构建缓冲区。
首先,在ModelObject
类的顶部,为数据列表声明变量:
Vector<Short> faces=new Vector<Short>();
Vector<Short> vtPointer=new Vector<Short>();
Vector<Short> vnPointer=new Vector<Short>();
Vector<Float> v=new Vector<Float>();
Vector<Float> vn=new Vector<Float>();
Vector<Material> materials=null;
让我们用助手方法的占位符来编写parseObj
。我们打开文件,处理每一行,构建缓冲区,并处理潜在的 IO 错误:
void parseObj(InputStream inputStream) {
BufferedReader reader = null;
String line = null;
reader = new BufferedReader(new InputStreamReader(inputStream));
if (reader == null)
return; // error
try { // try to read lines of the file
while ((line = reader.readLine()) != null) {
parseLine(line);
}
buildBuffers();
} catch (IOException e) {
e.printStackTrace();
}
}
parseLine
代码相当简单。该行的第一个标记是一个或两个字符的命令(如v
、vn
或f
,后跟数据值(浮点坐标或整数索引)。下面是parseLine
的代码和v
和vn
顶点的解析器:
private void parseLine(String line) {
Log.v("obj", line);
if(line.startsWith("f")){//a polygonal face
processFLine(line);
}
else
if(line.startsWith("vn")){
processVNLine(line);
}
else
if(line.startsWith("v")){ //line having geometric position of single vertex
processVLine(line);
}
}
private void processVLine(String line){
String [] tokens=line.split("[ ]+");
//split the line at the spaces
int c=tokens.length;
for(int i=1; i<c; i++){ //add the vertex to the vertex array
v.add(Float.valueOf(tokens[i]));
}
}
private void processVNLine(String line){
String [] tokens=line.split("[ ]+");
//split the line at the spaces
int c=tokens.length;
for(int i=1; i<c; i++){ //add the vertex to the vertex array
vn.add(Float.valueOf(tokens[i]));
}
}
f
线需要处理各种价值情况。
至于指定面索引的f
命令,它们是索引到顶点列表中的整数值。当有三个索引时,它描述一个三角形,四个描述一个四边形,依此类推。任何超过三条边的东西都需要细分成三角形,以便我们用 OpenGL ES 渲染。
此外,索引值可以有任何组合,包括v
或v/vt
或v/vt/vn
等格式,甚至还有v//vn
、/vt/vn
或//vn
等格式。(请记住,由于我们没有映射纹理,我们将只使用第一个和第三个。)
让我们先处理最简单的情况,一个三角形面:
private void processFLine(String line){
String [] tokens=line.split("[ ]+");
int c=tokens.length;
if(tokens[1].matches("[0-9]+")){//f: v
if(c==4){//3 faces
for(int i=1; i<c; i++){
Short s=Short.valueOf(tokens[i]);
s--;
faces.add(s);
}
}
}
}
现在考虑一下脸上有三个以上的指数。我们需要一种三角化多边形的方法。让我们现在写下来:
public static Vector<Short> triangulate(Vector<Short> polygon){
Vector<Short> triangles=new Vector<Short>();
for(int i=1; i<polygon.size()-1; i++){
triangles.add(polygon.get(0));
triangles.add(polygon.get(i));
triangles.add(polygon.get(i+1));
}
return triangles;
}
我们可以在processFLine
中使用它:
private void processFLine(String line) {
String[] tokens = line.split("[ ]+");
int c = tokens.length;
if (tokens[1].matches("[0-9]+") || //f: v
tokens[1].matches("[0-9]+/[0-9]+")) {//f: v/vt
if (c == 4) {//3 faces
for (int i = 1; i < c; i++) {
Short s = Short.valueOf(tokens[i]);
s--;
faces.add(s);
}
}
else{//more faces
Vector<Short> polygon=new Vector<Short>();
for(int i=1; i<tokens.length; i++){
Short s=Short.valueOf(tokens[i]);
s--;
polygon.add(s);
}
faces.addAll(triangulate(polygon));
//triangulate the polygon and //add the resulting faces
}
}
//if(tokens[1].matches("[0-9]+//[0-9]+")){//f: v//vn
//if(tokens[1].matches("[0-9]+/[0-9]+/[0-9]+")){
//f: v/vt/vn
}
这个代码是应用于面值v
和v/vt
,因为我们正在跳过纹理。我还评论了面部指数值的另外两种排列。剩下的大部分只是蛮力字符串解析。v//vn
案如下:
if(tokens[1].matches("[0-9]+//[0-9]+")){//f: v//vn
if(c==4){//3 faces
for(int i=1; i<c; i++){
Short s=Short.valueOf(tokens[i].split("//")[0]);
s--;
faces.add(s);
s=Short.valueOf(tokens[i].split("//")[1]);
s--;
vnPointer.add(s);
}
}
else{//triangulate
Vector<Short> tmpFaces=new Vector<Short>();
Vector<Short> tmpVn=new Vector<Short>();
for(int i=1; i<tokens.length; i++){
Short s=Short.valueOf(tokens[i].split("//")[0]);
s--;
tmpFaces.add(s);
s=Short.valueOf(tokens[i].split("//")[1]);
s--;
tmpVn.add(s);
}
faces.addAll(triangulate(tmpFaces));
vnPointer.addAll(triangulate(tmpVn));
}
}
最后v/vt/vn
情况如下:
if(tokens[1].matches("[0-9]+/[0-9]+/[0-9]+")){//f: v/vt/vn
if(c==4){//3 faces
for(int i=1; i<c; i++){
Short s=Short.valueOf(tokens[i].split("/")[0]);
s--;
faces.add(s);
// (skip vt)
s=Short.valueOf(tokens[i].split("/")[2]);
s--;
vnPointer.add(s);
}
}
else{//triangulate
Vector<Short> tmpFaces=new Vector<Short>();
Vector<Short> tmpVn=new Vector<Short>();
for(int i=1; i<tokens.length; i++){
Short s=Short.valueOf(tokens[i].split("/")[0]);
s--;
tmpFaces.add(s);
// (skip vt)
s=Short.valueOf(tokens[i].split("/")[2]);
s--;
tmpVn.add(s);
}
faces.addAll(triangulate(tmpFaces));
vnPointer.addAll(triangulate(tmpVn));
}
}
如前所述,在 OBJ 文件格式描述中,索引可以是负数;在这种情况下,需要从顶点列表的末尾向后引用它们。这可以通过将索引值添加到索引列表的大小中来实现。为了支持这一点,在前面的代码中,替换所有s--
;具有以下内容的行:
if (s < 0)
s = (short)(s + v.size());
else
s--;
构建缓冲区
parseObj
方法的最后一步是从模型数据构建我们的着色器缓冲区,即vertexBuffer
、normalBuffer
和indexBuffer
变量。我们现在可以将它添加到一个buildBuffers
方法中,如下所示:
private void buildBuffers() {
numIndices = faces.size();
float[] tmp = new float[v.size()];
int i = 0;
for(Float f : v)
tmp[i++ ] = (f != null ? f : Float.NaN);
vertexBuffer = allocateFloatBuffer(tmp);
i = 0;
tmp = new float[vn.size()];
for(Float f : vn)
tmp[i++ ] = (f != null ? -f : Float.NaN);
//invert normals
normalBuffer = allocateFloatBuffer(tmp);
i = 0;
short[] indicies = new short[faces.size()];
for(Short s : faces)
indicies[i++ ] = (s != null ? s : 0);
indexBuffer = allocateShortBuffer(indicies);
}
一个警告。我们注意到对于RenderBox
坐标系和着色器,有必要反转 OBJ 数据的法线(使用-f
而不是f
)。实际上,这取决于 OBJ 出口商(3Ds Max、Blender 和 Maya)。有些会,有些不会翻转法线。不幸的是,除了查看模型,没有办法确定法线是否翻转。出于这个原因,一些 OBJ 导入器/查看器提供(可选的)函数来根据面几何计算法线,而不是依赖于导入数据本身。
模型范围、比例和中心
3D 模型有各种形状和尺寸。要在我们的应用中查看它们,我们需要知道模型的最小和最大边界及其几何中心,以便对其进行适当的缩放和定位。现在让我们将此添加到ModelObject
中。
在ModelObject
类的顶部,添加以下变量:
public Vector3 extentsMin, extentsMax;
在解析模型数据之前,初始化解析器中的范围。最小范围被初始化为最大可能值;最大范围被初始化为最小可能值:
public ModelObject(int objFile) {
super();
extentsMin = new Vector3(Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE);
extentsMax = new Vector3(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE);
. . .
我们将在导入过程中计算范围,而不是在模型加载后计算范围。当我们向顶点列表添加一个新顶点时,我们将计算当前范围。在processVLine
循环中添加对setExtents
的呼叫:
private void processVLine(String line) {
String[] tokens = line.split("[ ]+");
//split the line at the spaces
int c = tokens.length;
for (int i = 1; i < c; i++) { //add the vertex to the vertex array
Float value = Float.valueOf(tokens[i]);
v.add(value);
setExtents(i, value);
}
}
那么setExtents
方法可以实现如下:
private void setExtents(int coord, Float value) {
switch (coord) {
case 1:
if (value < extentsMin.x)
extentsMin.x = value;
if (value > extentsMax.x)
extentsMax.x = value;
break;
case 2:
if (value < extentsMin.y)
extentsMin.y = value;
if (value > extentsMax.y)
extentsMax.y = value;
break;
case 3:
if (value < extentsMin.z)
extentsMin.z = value;
if (value > extentsMax.z)
extentsMax.z = value;
break;
}
}
让我们添加一个标量方法,当我们将模型添加到场景中时会很有用(正如您将在下一个主题中看到的),使用范围-1
到1
将它缩放到规范化的大小:
public float normalScalar() {
float sizeX = (extentsMax.x - extentsMin.x);
float sizeY = (extentsMax.y - extentsMin.y);
float sizeZ = (extentsMax.z - extentsMin.z);
return (2.0f / Math.max(sizeX, Math.max(sizeY, sizeZ)));
}
现在,让我们试试吧!
我是一个小茶壶
几十年来,3D 计算机图形研究人员和开发人员一直在使用这个可爱的茶壶模型。真是经典!后面的故事是,著名的计算机图形学先驱和研究者马丁·纽维尔需要一个模型来做他的工作,他的妻子建议他在家里给他们的茶壶做模型。原件现在在波士顿计算机博物馆展出。我们在这本书的可下载文件中包含了这一经典模型的 OBJ 版本。
当然,你可以选择自己的 OBJ 文件,但是如果你想用茶壶,找到teapot.obj
文件,复制到res/raw
文件夹(必要时创建文件夹)。
现在加载模型并尝试它。在MainActivity
中,在MainActivity
类的顶部添加一个变量来保存当前模型:
Transform model;
将以下代码添加到setup
方法中。请注意,我们将它缩放到原始大小的一小部分,并将其放在摄像机前 3 个单位:
public void setup() {
ModelObject modelObj = new ModelObject(R.raw.teapot);
float scalar = modelObj.normalScalar();
model = new Transform()
.setLocalPosition(0, 0, -3)
.setLocalScale(scalar, scalar, scalar)
.addComponent(modelObj);
}
运行项目,它应该如下所示:
可以看到模型加载渲染成功。不幸的是,阴影很难辨别。为了更好地观察阴影茶壶,让我们把它向下移动一点。修改设置中的setLocalPosition
方法,如下:
.setLocalPosition(0, -2, -3)
下面的截图经过裁剪和放大,因此您可以在这里看到阴影茶壶,类似于您在纸板查看器中看到的样子:
我是一个小小的旋转茶壶
让我们通过在用户旋转头部的同时旋转模型来增强观看体验。效果将不同于“正常”的虚拟现实体验。通常,在虚拟现实中移动头部会旋转场景中相机的主观视图,以便与头部移动一致地环顾四周。在这个项目中,头部的移动就像一个旋转模型的输入控件。模特一直在你面前的固定位置。
实现这个特性非常简单。在每一帧的开始调用RenderBox preDraw
接口方法。我们将获得当前的头部角度,并相应地旋转模型,将头部后欧拉角转换为四元数。(组合多个欧拉角会导致意外的最终旋转方向)。我们还将共轭(即反转或反转)旋转,这样当你向上看时,你会看到物体的底部,等等。这样感觉更自然。
在MainActivity
中,给preDraw
添加以下代码:
public void preDraw() {
float[] hAngles = RenderBox.instance.headAngles;
Quaternion rot = new Quaternion();
rot.setEulerAnglesRad(hAngles[0], hAngles[1], hAngles[2]);
model.setLocalRotation(rot.conjugate());
}
在setup
中,确保setLocalPosition
方法将茶壶放在摄像机正前方:
.setLocalPosition(0, 0, -3)
试着运行它。我们快到了!模型随着头部旋转,但我们仍然在虚拟现实空间中四处张望。
要锁定头部位置,我们只需要在RenderBox
中禁用头部跟踪。如果您的版本RenderBox
(内置于第 5 章、RenderBox 引擎)还没有这个功能,请将其添加到您单独的RenderBoxLib
lib 项目中,如下所示:
在Camera.java
文件中,首先为headTracking
添加一个新的公共变量:
public boolean headTracking = true;
修改onDrawEye
方法,有条件地更新视图变换,如下所示:
if (headTracking) {
// Apply the eye transformation to the camera.
Matrix.multiplyMM(view, 0, eye.getEyeView(), 0, camera, 0);
} else {
// copy camera into view
for (int i=0; i < camera.length; i++) { view[i] = camera[i]; }
}
重建后,确保将更新后的.aar
文件复制到ModelViewer
项目的RenderBox
模块文件夹中。
现在,在MainActivity
类的setup()
中,添加以下设置:
RenderBox.instance.mainCamera.headTracking = false;
现在运行它,当你移动你的头时,模型保持相对静止,但随着你转动你的头而旋转。好极了。好多了。
线程安全
在第 7 章、 360 度画廊中,我们解释了工作线程从渲染线程卸载处理的需要。在这个项目中,我们将向ModelObject
构造函数添加线程,在这里我们读取并解析模型文件:
public ModelObject(final int objFile) {
super();
extentsMin = new Vector3(Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE);
extentsMax = new Vector3(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE);
SolidColorLightingMaterial.setupProgram();
enabled = false;
new Thread(new Runnable() {
@Override
public void run() {
InputStream inputStream = RenderBox.instance.mainActivity.getResources().openRawResource(objFile);
if (inputStream == null)
return; // error
createMaterial();
enabled = true;
float scalar = normalScalar();
transform.setLocalScale(scalar, scalar, scalar);
}
}).start();
}
我们必须将文件句柄声明为final
,以便能够从内部类中访问它。你可能也注意到了,我们在启动线程之前给素材的setup
程序添加了一个调用,以确保它被及时正确地设置,并避免应用崩溃。这避免了在queueEvent
过程中调用createMaterial
的需要,因为着色器编译器利用了图形上下文。同样,我们禁用该对象,直到它完成数据加载。最后,由于负载是异步的,因此有必要在此过程结束时设置刻度。我们之前的方法在setup()
中设置比例,现在在模型加载完成之前完成。
有目的地发射
在第 7 章360 度图库中,我们介绍了使用安卓意图将应用与特定文件类型相关联,以便作为这些文件的查看者启动我们的应用。我们将在这里对 OBJ 文件进行同样的操作。
一个的意图是任何应用都可以向安卓系统发送的消息,该消息声明它打算为某个目的使用另一个应用。意图对象包含许多成员来描述需要执行什么类型的操作,以及需要对其执行的数据(如果有的话)。对于图像库,我们将意图过滤器与图像 mime 类型相关联。对于这个项目,我们将把一个意图过滤器和一个文件扩展名关联起来。
**在您的AndroidManifest.xml
文件中,向活动块添加一个意图过滤器。这让安卓知道该应用可以用作 OBJ 文件查看器。我们需要将其指定为文件方案和文件名模式。安卓也需要通配符 mime 类型和主机。添加以下 XML 代码:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.obj" />
<data android:host="*" />
</intent-filter>
为了处理这种情况,我们将向ModelObject
添加一个新的构造函数,它采用 URI 字符串而不是资源标识,就像我们之前所做的那样。像其他构造函数一样,我们需要打开一个输入流并将其传递给parseObj
。下面是构造函数,包括工作线程:
public ModelObject(final String uri) {
super();
extentsMin = new Vector3(Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE);
extentsMax = new Vector3(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE);
SolidColorLightingMaterial.setupProgram();
enabled = false;
new Thread(new Runnable() {
@Override
public void run() {
File file = new File(uri.toString());
FileInputStream fileInputStream;
try {
fileInputStream = new FileInputStream(file);
} catch (IOException e) {
e.printStackTrace();
return; // error
}
parseObj(fileInputStream);
createMaterial();
enabled = true;
float scalar = normalScalar();
transform.setLocalScale(scalar, scalar, scalar);
}
}).start();
}
现在在MainActivity
类的setup
中,我们将检查该应用是否是从一个意图启动的,并使用该意图 URI。否则,我们将查看默认模型,就像我们之前所做的那样:
public void setup() {
ModelObject modelObj;
Uri intentUri = getIntent().getData();
if (intentUri != null) {
Log.d(TAG, "!!!! intent " + intentUri.getPath());
modelObj = new ModelObject(intentUri.getPath());
} else {
// default object
modelObj = new ModelObject(R.raw.teapot);
}
//...
现在项目已经完成并安装在手机上,让我们尝试一些网络集成。打开网络浏览器,访问三维模型下载网站。
找到感兴趣车型的下载链接下载到手机,然后出现提示时,使用ModelViewer
app 查看!
实用且生产准备就绪
请注意,正如前面提到的,我们已经创建了 OBJ 模型格式的有限实现,因此在这一点上,并不是您找到的每个模型都能正确查看(如果有的话)。话说回来,这可能就足够了,这取决于您自己项目的需求,例如,如果您在资源文件夹中包含特定的模型,这些模型可以在您的应用的发布版本中查看。当您完全控制输入数据时,您可以偷工减料。
虽然 OBJ 文件格式的基本结构不是很复杂,但正如我们在这里所展示的,就像软件(和生活)中的许多事情一样,“魔鬼在细节中。”使用这个项目作为起点,然后构建您自己的实用的和生产就绪的 OBJ 文件解析器和渲染器将需要大量的额外工作。您还可以对预先存在的包、其他模型格式进行一些研究,甚至可以从像 LibGDX 这样的开源游戏引擎中提取一些代码。我们忽略但值得考虑的 OBJ 特色包括:
- 纹理顶点
- 材质定义
- 曲线元素
- 几何分组
- 颜色和其他顶点属性
总结
在这个项目中,我们用开放的 OBJ 文件格式为三维模型编写了一个简单的查看器。我们实现了一个ModelObject
类,该类解析模型文件并构建RenderBox
渲染场景中的对象所需的向量和法线缓冲区。然后我们启用了阴影。然后我们让观众互动,这样当你移动你的头时,模型就会旋转。
在下一章,我们将探索另一种媒体,你的音乐。音乐可视化器响应当前的音乐播放器,在虚拟现实世界中显示跳舞的几何图形。**
版权属于:月萌API www.moonapi.com,转载请注明出处