实际开发中,大多数项目,通常是3D美术设计师或建筑、机械等行业工程师提供的由3dmx、blender、substence、Solidworks等软件创建好的三维模型文件。
本章节第一小节以Threejs引擎自身为例,讲解Threejs模型导入导出,该小节对Threejs模型文件本身进行了讲解,让你明白你加载的三维模型文件里面都是包含什么内容。其它章节展示了一些加载外部不同格式模型的案例,除了讲解实现代码外,还对模型的数据结构进行了简单讲解,尽管加载一个模型不一定需要知道它的内容结构,但是如果你理解模型本质上就是顶点、材质等数据,那么随意给你一种格式模型,你至少会有了解该模型能够包含哪些数据的意识。
你可以通过下面代码导出模型的各类信息,然后在浏览器控制台打印出来模型数据,然后复制浏览器控制台模型数据粘贴到json文件中,最后可以尝试加载解析这些Threejs导出的json文件。之所以这么做,是为了让你理解其它三维软件,比如3dmax、blender软件导出的三维模型文件本质上是什么。
查看Threejs文档Geometry、Material、Light、Object3D等类,你可以发现这些类都提供了一个方法.toJSON()通过这个方法可以导出Threejs三维模型的各类数据,该方法的功能就是把Threejs的几何体、材质、光源等对象转化为JSON格式导出。
导出几何体信息。
var geometry = new THREE.BoxGeometry(100, 100, 100);
// 控制台查看立方体数据
console.log(geometry);
// 控制台查看geometry.toJSON()结果
console.log(geometry.toJSON());
// JSON对象转化为字符串
console.log(JSON.stringify(geometry.toJSON()));
// JSON.stringify()方法内部会自动调用参数的toJSON()方法
console.log(JSON.stringify(geometry));
导出材质信息。
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff,
}); //材质对象Material
console.log(material);
console.log(material.toJSON());
console.log(JSON.stringify(material));
导出场景scene信息。
var mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
scene.add(mesh); //网格模型添加到场景中
console.log(scene);
console.log(scene.toJSON());
实际开发中,加载一种特定格式的模型文件,Threejs在three.js-master\examples\js\loaders目录下会提供一系列的加载器,这些加载器本质上都是解析模型文件的字符串,通过正则表达式提取相关的顶点、材质等信息转化为Threejs自身的类表示的对象。为了让大家更理解这些常见的加载器,课件中提供了一个源码案例,编写了一个非常非常简易的自定义加载器,然后使用自定义的加载器去加载一个文件,让大家明白这些加载器怎么来的。
// 如果编写通用的材质加载器需要枚举所有的材质,这里没有列举完
var typeAPI = {
MeshLambertMaterial: THREE.MeshLambertMaterial,
MeshBasicMaterial: THREE.MeshBasicMaterial,
MeshPhongMaterial: THREE.MeshPhongMaterial,
PointsMaterial: THREE.PointsMaterial,
}
// 创建一个文件加载器,该加载器是对异步加载的封装
var loader = new THREE.FileLoader();
loader.load('material.json', function(elem) {
console.log(elem);// 查看加载返回的内容
var obj = JSON.parse(elem);// 字符串转JSON对象
console.log(obj);// 查看转化结果
var geometry = new THREE.BoxGeometry(100, 100, 100);
/**
* 解析材质数据
*/
// 根据type的值判断调用threejs的哪一个API
var material = new typeAPI[obj.type]();
// 从obj.color中提取颜色
// 16711935对应颜色0xFF00FF 255对应颜色0x0000FF
material.color.r = (obj.color >> 16 & 255) / 255; //获取颜色值R部分
material.color.g = (obj.color >> 8 & 255) / 255; //获取颜色值G部分
material.color.b = (obj.color & 255) / 255; //获取颜色值B部分
var mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
scene.add(mesh); //网格模型添加到场景中
})
缓冲几何体数据加载器。
/**
*
*/
var loader = new THREE.BufferGeometryLoader();
loader.load('bufferGeometry.json',function (geometry) {
// 控制台查看加载放回的threejs对象结构
console.log(geometry);
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff,
}); //材质对象Material
var mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
scene.add(mesh); //网格模型添加到场景中
})
网格模型Mesh加载,包含几何体Geometry和材质Material
var loader = new THREE.ObjectLoader();
loader.load('model.json',function (obj) {
console.log(obj);
console.log(obj.type);
obj.scale.set(100,100,100)
scene.add(obj)
})
加载组Group对象,模型对象构成的树结构
loader.load('group.json', function(obj) {
console.log(obj);
console.log(obj.type);
scene.add(obj)
})
加载场景对象,场景对象不仅包含模型,还包括光源对象
loader.load('scene.json',function (obj) {
console.log(obj);
console.log(obj.type);
obj.scale.set(100,100,100)
scene.add(obj)
})
基本所有的三维软件都支持导出.stl格式的三维模型文件,.stl格式的三维模型不包含材质Material信息,只包含几何体顶点数据的信息,你可以简单地把stl文件理解为几何体对象Geometry,本节课素材box.STL是一个立方体, 你可以用记事本或代码编辑器打开文件box.STL查看stl的数据结构。
.stl文件格式的数据结构,对于大多数普通开发者来说,如果仅仅为了加载显示一个三维模型,也没必要掌握,这里之所以要强调,不是为了让你记住,而是为了从底层了解Threejs模型加载的原理,达到举一反三的目的
三个位置坐标和一个三角形面的法线方向向量是一组数据,这一组数据表示一个三角形面的信息,可以回忆下第二章关于三角形面Face3的讲解。
表示一个三角形面信息的一组数据
//三角面1
facet normal 0 0 -1 //三角形面法向量
outer loop
vertex 50 50 -50 //顶点位置
vertex 50 -50 -50 //顶点位置
vertex -50 50 -50 //顶点位置
endloop
endfacet
一个立方体有6个矩形平面,每个矩形平面至少需要两个三角形拼接而成。那么立方体6个矩形平面至少需要12个三角形面构成,你可以查看文件box.STL中的12个三角形信息。
solid box //文件名字
//三角面1
facet normal 0 0 -1 //三角形面法向量
outer loop
vertex 50 50 -50 //顶点位置
vertex 50 -50 -50 //顶点位置
vertex -50 50 -50 //顶点位置
endloop
endfacet
//三角面2
facet normal 0 0 -1 //三角形面法向量
outer loop
vertex -50 50 -50 //顶点位置
vertex 50 -50 -50 //顶点位置
vertex -50 -50 -50 //顶点位置
endloop
endfacet
facet normal 0 1 0
.....
.....
//三角面12
facet normal -1 0 0
outer loop
vertex -50 -50 -50
vertex -50 50 50
vertex -50 50 -50
endloop
endfacet
endsolid
如果你想通过Threejs加载.stl格式三维模型文件,可以使用Threejs提供的一个扩展库stl加载器STLLoader.js,你可以在Three.js-master包中找到STLLoader.js文件,具体路径是three.js-master\examples\js\loaders.
.html文件中引入Threejs的扩展库STLLoader.js,引入该文件后,就可以在代码中使用构造函数THREE.STLLoader()实例化一个加载器。
<!--引入STLLoader.js文件-->
<script src="STLLoader.js"></script>
通过构造函数THREE.STLLoader()可以把.stl文件中几何体顶点信息提取出来转化为Three.js自身格式的几何体对象BufferGeometry。如果你有兴趣可以阅读STLLoader.js源码,尤其是你想独立开发自己公司特定格式加载器的情况下,更有必要参照学习。
// THREE.STLLoader创建一个加载器
var loader = new THREE.STLLoader();
loader.load('立方体.stl',function (geometry) {
// 加载完成后会返回一个几何体对象BufferGeometry,你可以通过Mesh、Points等方式渲染该几何体
})
/**
* stl数据加载
*/
var loader = new THREE.STLLoader();
// 立方体默认尺寸长宽高各200
loader.load('立方体.stl',function (geometry) {
// 控制台查看加载放回的threejs对象结构
console.log(geometry);
// 查看顶点数,一个立方体6个矩形面,每个矩形面至少2个三角面,每个三角面3个顶点,
// 如果没有索引index复用顶点,就是说一个立方体至少36个顶点
console.log(geometry.attributes.position.count);
// 缩放几何体
// geometry.scale(0.5,0.5,0.5);
// 几何体居中
// geometry.center();
// 平移立方体
// geometry.translate(-50,-50,-50);
var material = new THREE.MeshLambertMaterial({
color: 0x0000ff,
}); //材质对象Material
var mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
scene.add(mesh); //网格模型添加到场景中
})
你可以使用下面代码代替上面代码,查看Three.js如果通过点模型Points渲染stl文件中的顶点数据。
/**
* 点渲染模式
*/
loader.load('离心叶轮.stl',function (geometry) {
var material = new THREE.PointsMaterial({
color: 0x000000,
size: 0.5//点对象像素尺寸
}); //材质对象
var points = new THREE.Points(geometry, material); //点模型对象
scene.add(points); //点对象添加到场景中
})
使用三维软件导出.obj模型文件的时候,会同时导出一个材质文件.mtl, .obj和.stl文件包含的信息一样都是几何体顶点相关数据,材质文件.mtl包含的是模型的材质信息,比如颜色、贴图路径等。
加载.obj三维模型的时候,可以只加载.obj文件,然后借助three.js引擎自定义材质Material,也可以同时加载.obj和.mtl文件。
只加载obj文件,引入路径three.js-master/examples/js/loaders/OBJLoader.js下的OBJLoader.js文件即可
<!-- 引入obj模型加载库OBJLoader.js -->
<script src="../../three.js-master/examples/js/loaders/OBJLoader.js"></script>
文件加载
/**
* OBJ文件加载 只加载obj文件中的几何信息,不加载材质文件.mtl
*/
var loader = new THREE.OBJLoader();
// 没有材质文件,系统自动设置Phong网格材质
loader.load('./立方体/box.obj',function (obj) {
// 控制台查看返回结构:包含一个网格模型Mesh的组Group
console.log(obj);
// 查看加载器生成的材质对象:MeshPhongMaterial
console.log(obj.children[0].material);
scene.add(obj);
})
加载文件返回的对象插入场景中后,你也可以做一些自定的设置,比如缩放、居中等操作。
// 加载后的一些编辑操作
obj.children[0].scale.set(20,20,20);//网格模型缩放
obj.children[0].geometry.center();//网格模型的几何体居中
obj.children[0].material.color.set(0xff0000);//设置材质颜色
mtl文件包含了模型的材质信息,比如模型颜色、透明度等信息,还有纹理贴图的路径,比如颜色贴图、法线贴图、高光贴图等等。
<!-- 引入obj模型加载库OBJLoader.js -->
<script src="../../three.js-master/examples/js/loaders/OBJLoader.js"></script>
<!-- 引入obj模型材质加载库MTLLoader.js -->
<script src="../../three.js-master/examples/js/loaders/MTLLoader.js"></script>
/**
* OBJ和材质文件mtl加载
*/
var OBJLoader = new THREE.OBJLoader();//obj加载器
var MTLLoader = new THREE.MTLLoader();//材质文件加载器
MTLLoader.load('./立方体/box.mtl', function(materials) {
// 返回一个包含材质的对象MaterialCreator
console.log(materials);
//obj的模型会和MaterialCreator包含的材质对应起来
OBJLoader.setMaterials(materials);
OBJLoader.load('./立方体/box.obj', function(obj) {
console.log(obj);
obj.scale.set(10, 10, 10); //放大obj组对象
scene.add(obj);//返回的组对象插入场景中
})
})
obj文件可以包含多个网格模型对象,不一定就是一个,这些网格模型对象全部是并列关系,无法通过父子关系构建一个树结构层级模型。
// 没有材质文件,系统自动设置Phong网格材质
OBJLoader.load('./多个模型/model.obj',function (obj) {
// 控制台查看返回结构:包含一个网格模型Mesh的组Group
console.log(obj);
scene.add(obj);
// 加载后的一些编辑操作
obj.scale.set(20,20,20);//网格模型缩放
// 设置其中一个网格模型的颜色
obj.children[0].material.color.set(0xff0000);
})
obj模型的mtl文件可能包含纹理贴图,也可能不包含,这主要看3D美术是否设置。
一个包含纹理贴图路径的.mtl文件,如果路径有问题,可能会无法加载,可以仿照该案例修改。
// 一个包含纹理贴图路径的.mtl文件
newmtl material_1
Ns 32
d 1
Tr 0
Tf 1 1 1
illum 2
Ka 0.5880 0.5880 0.5880
Kd 0.9880 0.9880 0.9880
Ks 0.1200 0.1200 0.1200
map_Kd ./贴图/Earth.png
map_ks ./贴图/EarthSpec.png
norm ./贴图/EarthNormal.png
mtl和threejs贴图对应关系
mtl贴图 | Threejs贴图 |
---|---|
map_kd | map |
map_ks | specularMap |
map_bump/bump | bumpMap |
/**
* OBJ和材质文件mtl加载
*/
var OBJLoader = new THREE.OBJLoader(); //obj加载器
var MTLLoader = new THREE.MTLLoader(); //材质文件加载器
MTLLoader.load('./贴图/material.mtl', function(materials) {
// 返回一个包含材质的对象MaterialCreator
console.log(materials);
//obj的模型会和MaterialCreator包含的材质对应起来
OBJLoader.setMaterials(materials);
OBJLoader.load('./贴图/model.obj', function(obj) {
console.log(obj);
scene.add(obj); //返回的组对象插入场景中
// 加载后操作
obj.children[0].scale.set(2, 2, 2); //缩放球体网格模型
// 通过调节参数,地球表面的凹凸感更强
obj.children[0].material.normalScale.set(3, 3);
})
})
3dmax导出的obj和mtl模型文件有时候需要修改一下个别位置字符,比如.obj中.mtl文件的名称可能是乱码mtllib �����.mtl,.mtl文件中贴图的路径要设置正确,比如导出的是绝对路径还是相对路径,可以打开看下。
.obj文件不包含场景的相机Camera、光源Light等信息,不能导出骨骼动画、变形动画,如果希望导出光照信息、相机信息、骨骼动画信息、变形动画信息,可以选择.fbx、.gltf等格式。
引入FBX加载器相关文件
<!-- 引入fbx模型加载库FBXLoader -->
<script src="http://www.yanhuangxueyuan.com/versions/threejsR92/examples/js/loaders/FBXLoader.js"></script>
<!-- 辅助文件 -->
<script src="http://www.yanhuangxueyuan.com/versions/threejsR92/examples/js/libs/inflate.min.js"></script>
加载模型文件,加载完成后,如果模型显示位置不符合要求,可以让3D美术修改,也可以通过Threejs程序进行平移、缩放等操作。
var loader = new THREE.FBXLoader();//创建一个FBX加载器
loader.load("SambaDancing.fbx", function(obj) {
// console.log(obj);//查看加载后返回的模型对象
scene.add(obj)
// 适当平移fbx模型位置
obj.translateY(-80);
})
stl、obj都是静态模型,不可以包含动画,fbx除了包含几何、材质信息,可以存储骨骼动画等数据。
解析之前可以先在浏览器控制台查看动画相关的数据是如何存储的。你可以看到obj.animations属性的数组包含两个剪辑对象AnimationClip,obj.animations[0]对应剪辑对象AnimationClip包含多组关键帧KeyframeTrack数据,obj.animations[1]对应的剪辑对象AnimationClip没有关键帧数据,也就是说没有关键帧动画。具体的开发中,可能美术提供的模型有很多包含关键帧动画的剪辑对象AnimationClip,你可以根据自己的需要解析某个剪辑对象AnimationClip对应的动画。
var loader = new THREE.FBXLoader();//创建一个FBX加载器
loader.load("SambaDancing.fbx", function(obj) {
...
// 可以在控制台打印obj对象,找到animations属性
console.log(obj)
// 查看动画数据 2个剪辑对象AnimationClip,一个有关键帧动画,一个没有
console.log(obj.animations)
})
var mixer=null;//声明一个混合器变量
var loader = new THREE.FBXLoader();//创建一个FBX加载器
loader.load("SambaDancing.fbx", function(obj) {
// console.log(obj)
scene.add(obj)
obj.translateY(-80);
// obj作为参数创建一个混合器,解析播放obj及其子对象包含的动画数据
mixer = new THREE.AnimationMixer(obj);
// 查看动画数据
console.log(obj.animations)
// obj.animations[0]:获得剪辑对象clip
var AnimationAction=mixer.clipAction(obj.animations[0]);
// AnimationAction.timeScale = 1; //默认1,可以调节播放速度
// AnimationAction.loop = THREE.LoopOnce; //不循环播放
// AnimationAction.clampWhenFinished=true;//暂停在最后一帧播放的状态
AnimationAction.play();//播放动画
})
// 创建一个时钟对象Clock
var clock = new THREE.Clock();
// 渲染函数
function render() {
renderer.render(scene, camera); //执行渲染操作
requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
if (mixer !== null) {
//clock.getDelta()方法获得两帧的时间间隔
// 更新混合器相关的时间
mixer.update(clock.getDelta());
}
}
render();
在线预览产品的时候,一个产品可能会提供不同的系列,比如颜色不同、造型款式不同。本节课的玉镯案例模型提供了三种颜色款式,不同的颜色款式本质上就是网格模型的颜色贴图.map不同。
model.obj文件中已经包含纹理映射的UV坐标数据,直接给模型颜色贴图属性.map赋值即可。
/**
* OBJ文件加载 只加载obj文件中的几何信息,不加载材质文件.mtl
*/
var loader = new THREE.OBJLoader();
loader.load('./model.obj',function (obj) {
// 控制台查看返回结构:包含一个网格模型Mesh的组Group
// console.log(obj);
//加载纹理贴图texture1.png
var texture = new THREE.TextureLoader().load('texture1.png');
// 颜色贴图中已经包含了光照信息,所以直接使用不受光照影响的基础网格材质MeshBasicMaterial
obj.children[0].material= new THREE.MeshBasicMaterial({
map:texture,//设置颜色纹理贴图
})
scene.add(obj);
obj.children[0].scale.set(5,5,5);//网格模型缩放
})
模型提供了texture1.png、texture2.png和texture3.png三张贴图,上面代码加载了texture1.png作为颜色贴图。
你可以在上面代码.load()回调函数中插入下面语句,给玉镯设置其它的颜色款式。
// 更换纹理贴图
var texture = new THREE.TextureLoader().load('texture2.png');
obj.children[0].material.map=texture
实际开发的时候,可能会通过前端UI进行颜色交互(颜色交互体验),其实实现也比较简单,比如一个按钮表示绿色,前端代码只要给该按钮绑定一个函数,函数中执行代码mesh.material.map=Texture改变颜色贴图属性.map的值即可。不过本节课主要是讲解Threejs,关于前端交互内容就不做过多描述,你可以自己创建一个按钮测试这个思路。
加载一个心脏的次时代模型,模型包含颜色贴图.map、法线贴图.normalMap、高光贴图.specularMap、环境贴图.envMap,关于这些贴图的相关属性可以查看高光网格模型材质的文档MeshPhongMaterial。
/**
* OBJ文件加载 只加载obj文件中的几何信息,不加载材质文件.mtl
*/
var loader = new THREE.OBJLoader();
// 没有材质文件,系统自动设置Phong网格材质
var mesh = null; //声明一个网格模型变量
loader.load('./heart/model.obj', function(obj) {
// 控制台查看返回结构:包含一个网格模型Mesh的组Group
console.log(obj);
scene.add(obj);
mesh = obj.children[0]; //获得心脏网格模型
mesh.scale.set(10, 10, 10); //网格模型缩放
// 创建一个纹理加载器
var textureLoader = new THREE.TextureLoader();
...
})
设置模型的颜色贴图.map。
//加载颜色纹理
var texture = textureLoader.load('./heart/color.png');
mesh.material.map = texture;
设置模型的法线贴图.normalMap,表面细节更丰富,为了压缩模型顶点数量,也就是降低文件大小,3D美术通常会给程序员提供法线贴图。
var textureNormal = textureLoader.load('./heart/normal.png');
mesh.material.normalMap = textureNormal
// 设置深浅程度
mesh.material.normalScale.set(1.5, 1.5)
对于心脏模型而言,模型外表面不同区域的粗糙度不同,对光线的镜面反射程度不同,所以可以把这些不同区域的不同光反射信息记录在一个贴图上,即高光贴图.specularMap,设置高光贴图需要高光网格模型材质MeshPhongMaterial。
// 设置高光贴图,一个网格模型不同的区域反射光线的能力不同
var textureSpecular = textureLoader.load('./heart/Specular.png');
mesh.material.specularMap = textureSpecular;
mesh.material.specular.set(0xffffff);// 高光反射颜色
mesh.material.shininess = 100;// 高光高亮程度,默认30
通过类CubeTextureLoader来加载六张纹理贴图’px.jpg’, ‘nx.jpg’, ‘py.jpg’, ‘ny.jpg’, ‘pz.jpg’, ‘nz.jpg’。
设置环境贴图.envMap,反射周围环境效果,渲染更逼真。
var textureCube = new THREE.CubeTextureLoader()
.setPath('环境贴图/')
.load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']);
mesh.material.envMap = textureCube;
渲染模型的时候,合理的设置光源是很必要的,比如光源强度太低,模型就会比较灰暗,光源强度太高,模型会过于明亮。
/**
* 光源设置
*/
//点光源
var point = new THREE.PointLight(0xffffff, 0.3);
point.position.set(400, 200, 300); //点光源位置
scene.add(point); //点光源添加到场景中
// 环境光
var ambient = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambient);
// 方向光1
var directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(400, 200, 300);
scene.add(directionalLight);
// 方向光2
var directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(-400, -200, -300);
scene.add(directionalLight);