做有态度的前端团队

网易FEG前端团队

#ThreeJS学习笔记( 二)——导入外部模型

外部模型格式

Threejs支持了许多格式的3D模型导入,包括*.obj、 *.sea、*.3mf 、*.amf、*.sea、*.pmd、*.json等。
这里主要讲解一下obj模型的导入,及将obj文件转成文件更小的json格式导入。

导入obj模型

3Dmax格式转换成obj格式

美术提供的一般为3Dmax项目文件夹,里面包含了.max文件以及贴图图片等资源,用3DMAX打开.max文件可以看到3D模型
如图:

1469608235365.png

  • 点击菜单里的导出,选择obj格式,点击保存后出现选项
  • 导出比例:几何体选项里的输出比例,默认是1.0,我们根据模型的分辨率,以及需要在网页上呈现的分辨率设置一下比例输出即可。简单的说,你直接按1.0比例输出的如果在浏览器看的时候大小差距太大了,可以在这里导出比例设置一下,另外也可以设置mesh的scale,直接缩放。这个模型需要将比例设置为0.1。几何体的其他选项都不需要修改。
  • 材质:点击材质导出,勾选使用材质路径,选择保存材质的路径。格式为jpg,格式设置中可以设置导出jpg的质量,无特殊要求默认即可,并勾选转换位图,关闭材质设置。
  • 点击导出,导出过程有可能会提示找不到材质,设置材质路径为美术提供的文件夹目录即可
  • 最后会得到一个*.obj文件、一个*mtl文件、若干材质图,包括多个不同通道的贴图jpg和法线贴图jpg

1469608270416.png

1469608312275.png

1469608413927.png

1469610128497.png

导出obj后的二次处理

导出后的文件如图所示

1469613142241.png

其中,test.mtl文件定义了test.obj模型与各贴图间的对应关系。
由于示例中的模型是由多个子模型组成的模型组,同时设置了多通道贴图,每一个子模型都对应四张贴图。
打开mtl文件,我们可以看到每个子模型都有一组贴图数据

# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# 创建的文件:27.07.2016 17:00:50

newmtl JZ2012_jz_l_1  
    Ns 20.0000
    Ni 1.5000
    d 1.0000
    Tr 0.0000
    Tf 1.0000 1.0000 1.0000 
    illum 2
    Ka 0.5882 0.5882 0.5882
    Kd 0.5882 0.5882 0.5882
    Ks 0.4500 0.4500 0.4500
    Ke 0.0000 0.0000 0.0000
    map_Ka JZ2012_jz_d.jpg
    map_Kd JZ2012_jz_d.jpg
    map_Ks JZ2012_jz_s.jpg
    map_Ke JZ2012_jz_e.jpg
    map_bump Map__171_法线凹凸.jpg

newmtl JZ2030_jz_l_1    
......

例如上面例子中关于子模型JZ2012_jz_l_1 贴图描述,其中map_Ka,map_Kd,map_Ks,map_Ke指定了各通道对应的贴图,map_bump则指定了法线贴图:

1469613671197.png

在使用threejs的mtlLoader解析mtl贴图并渲染为模型材质时,threejs仅调用map_Ka通道图片和map_bump法线贴图进行材质渲染。而基于图片数量及文件大小的控制,项目中采用的方法是让设计师将 JZ2012_jz_d.jpg、JZ2012_jz_s.jpg、JZ2012_jz_e.jpg三张图合成并调色后作为map_Ka通道贴图:

1469623501953.png

注:目前未查到threeJS 的mtl_loader是否能自动载入多通道贴图,待研究。
另外由于自动导出的法线贴图名称中带有中文,需要将mtl中map_bump和对应的法向贴图名称改成非中文命名。
在做以上图片处理时,需要同时将图片资源做压缩处理,以免资源加载时间过长。

使用mtlLoader和objLoaer加载obj模型及贴图

加载带mtl材质的obj模型,需要先定义mtl对象并加载mtl之外再加载obj模型。此时需要引用threejs两个js库

    <script src="../js/lib/threejs/MTLLoader.js"></script>
    <script src="../js/lib/threejs/OBJLoader.js"></script>

THREE.MTLLoader()函数说明:

  • mtlLoader.setBaseUrl():设置材质路径
  • mtlLoader.setPath():设置mtl文件所在路径
  • mtlLoader.load(filename,onSuccess(materials ),onProgress(xhr),onError(error)):mtl文件名、 加载成功后回调处理(参数为生成的材质库)、加载过程中回调处理(xhr对象属性可计算出已完成加载百分比)、失败回调处理

THREE.OBJLoader() 函数说明:

  • objLoader.setMaterials( materials ):设置obj使用的材质贴图
  • objLoader.setPath( options.objPath ):设置obj文件所在路径
  • objLoader.load( filename,onSuccess(object ),onProgress(xhr),onError(error)):obj文件名、 加载成功后回调处理(参数为生成的三维对象)、加载过程中回调处理(xhr对象属性可计算出已完成加载百分比)、失败回调处理。

在onSuccess(object ){}回调里我们可以对生成的三维对象做一些处理:对材质进行调色、设置透明度、设置贴图模式等,对设置旋转、缩放、位置摆放、自发光颜色、环境光颜色。
如果obj文件代表的三维对象是由多个子模型构成的模型组合,我们可以调用object.traverse(function(child){})来对每个子模型进行处理。
以下简单封装成一个函数


function createMtlObj(options){
//      options={
//          mtlBaseUrl:"",
//          mtlPath:"",
//          mtlFileName:"",
//          objPath:"",
//          objFileName:"",
//          completeCallback:function(object){  
//          }
//          progress:function(persent){
//              
//          }
//      }
    THREE.Loader.Handlers.add( /\.dds$/i, new THREE.DDSLoader() );
    var mtlLoader = new THREE.MTLLoader();
    mtlLoader.setBaseUrl( options.mtlBaseUrl );//设置材质路径
    mtlLoader.setPath( options.mtlPath );//设置mtl文件路径
    mtlLoader.load( options.mtlFileName, function( materials ) {
        materials.preload();
        var objLoader = new THREE.OBJLoader();
        objLoader.setMaterials( materials );//设置三维对象材质库
        objLoader.setPath( options.objPath );//设置obj文件所在目录
        objLoader.load( options.objFileName, function ( object ) {
            
            
            if(typeof options.completeCallback=="function"){
                options.completeCallback(object);
            }
        }, function ( xhr ) {
            if ( xhr.lengthComputable ) {
                var percentComplete = xhr.loaded / xhr.total * 100;
                if(typeof options.progress =="function"){
                    options.progress( Math.round(percentComplete, 2));
                }
                //console.log( Math.round(percentComplete, 2) + '% downloaded' );
            }
        }, function(error){
            
        });

    });
}

调用示例:


createMtlObj({
    mtlBaseUrl:"../resource/haven/",
    mtlPath: "../resource/haven/",
    mtlFileName:"threejs.mtl",
    objPath:"../resource/haven/",
    objFileName:"threejs.obj",
    completeCallback:function(object){
        object.traverse(function(child) { 
            if (child instanceof THREE.Mesh) { 
                child.material.side = THREE.DoubleSide;//设置贴图模式为双面贴图
                child.material.emissive.r=0;//设置rgb通道R通道颜色
                child.material.emissive.g=0.01;//设置rgb通道G通道颜色
                child.material.emissive.b=0.05;//设置rgb通道B通道颜色
                child.material.transparent=true;//材质允许透明
                //child.material.opacity=0;//材质默认透明度                        
                //child.material.shading=THREE.SmoothShading;//平滑渲染
            }
        });
        object.emissive=0x00ffff;//自发光颜色
        object.ambient=0x00ffff;//环境光颜色
//      object.rotation.x= 0;//x轴方向旋转角度
        object.position.y = 0;//位置坐标X
        object.position.z = 0;//位置坐标y
        object.scale.x=1;//缩放级别
        object.scale.y=1;//缩放级别
        object.scale.z=1;//缩放级别
        object.name="haven";//刚体名称
        object.rotation.y=-Math.PI;//初始Y轴方向旋转角度
        scene.add(object);//添加到场景中
    },
    progress:function(persent){
        
        $("#havenloading .progress").css("width",persent+"%");
    }
})

此时如果在之前没有添加光源,在浏览器上看到的可能是一片黑色,通过增加光源可以将物体显现出来。比例添加一个全局光

var ambient = new THREE.AmbientLight( 0xffffff );
scene.add( ambient );

此时在浏览器看到是这样的效果:
1470037673302.png

导入obj模型的后期渲染

如上图看到,我们导入的模型经过调色虽然比3Dmax显示效果好些,但仍然一片灰蒙蒙,丑不拉叽的。所以后期需要适当添加各种光源来渲染出更好的效果。
添加一个平行光做为主光源营造反射光面,并添加三个点光源对暗部适当补光后效果如下:

var directionalLight = new THREE.DirectionalLight( 0xffffff );
directionalLight.position.set( -5, 5, 5).normalize();
scene.add( directionalLight );
var pointlight = new THREE.PointLight(0x63d5ff, 1, 200); 
pointlight.position.set(0, 0, 200);
scene.add( pointlight );                
var pointlight2 = new THREE.PointLight(0xffffff, 1, 200); 
pointlight2.position.set(-200, 200, 200);
scene.add( pointlight2 );
var pointlight3 = new THREE.PointLight(0xffffff, 1.5, 200); 
pointlight3.position.set(-200, 200, 0);
scene.add( pointlight3 );

1470039562664.png

接下再添加一个4000*4000*4000的天空盒子作为背景,并适当调整camera的可视范围,最后显示结果如下:

1470039862826.png

在设置点光源位置时可借助Threejs提供的helper来查看设置的点光源位置,方便调整光源位置

scene.add( new THREE.PointLightHelper( pointlight3 ) );
scene.add( new THREE.PointLightHelper( pointlight2 ) );
scene.add( new THREE.PointLightHelper( pointlight ) );

1470041255056.png

DEMO:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
    <div id="space"></div>  
    <script   src="https://code.jquery.com/jquery-1.12.4.min.js"   integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="   crossorigin="anonymous"></script>
    <script src="../js/lib/threejs/three.js"></script>
    <script src="../js/lib/threejs/MTLLoader.js"></script>
    <script src="../js/lib/threejs/OBJLoader.js"></script>
    <script src="../js/lib/threejs/OrbitControls.js"></script>
    <script>
        

        var container, stats;

        var camera, scene, renderer;

        var mouseX = 0, mouseY = 0;

        var windowHalfX = window.innerWidth / 2;
        var windowHalfY = window.innerHeight / 2;


        init();
        animate();
        var mesh;

        function init() {

            container = document.getElementById("space")
            camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 8000 );
            camera.position.set(0, 0, 1500);
            
            scene = new THREE.Scene();

            var ambient = new THREE.AmbientLight( 0xffffff );
            scene.add( ambient );
            
            
            var directionalLight = new THREE.DirectionalLight( 0xffffff );
            directionalLight.position.set( -5, 5, 5).normalize();
            scene.add( directionalLight );

            var pointlight = new THREE.PointLight(0x63d5ff, 1, 200); 
            pointlight.position.set(0, 0, 200);
            scene.add( pointlight );                
            var pointlight2 = new THREE.PointLight(0xffffff, 1, 200); 
            pointlight2.position.set(-200, 200, 200);
            scene.add( pointlight2 );
            var pointlight3 = new THREE.PointLight(0xffffff, 1.5, 200); 
            pointlight3.position.set(-200, 200, 0);
            scene.add( pointlight3 );
            scene.add( new THREE.PointLightHelper( pointlight3 ) );
            scene.add( new THREE.PointLightHelper( pointlight2 ) );
            scene.add( new THREE.PointLightHelper( pointlight ) );
        
            
            var path = "../resource/sky/";
            var format = '.jpg';
            var urls = [
                    path + 'px' + format, path + 'nx' + format,
                    path + 'py' + format, path + 'ny' + format,
                    path + 'pz' + format, path + 'nz' + format
                ];
            var skyMaterials = []; 
            for (var i = 0; i < urls.length; ++i) {
                var loader = new THREE.TextureLoader();
                loader.setCrossOrigin( this.crossOrigin );
                var texture = loader.load( urls[i], function(){}, undefined, function(){} );
                
                skyMaterials.push(new THREE.MeshBasicMaterial({
                    //map: THREE.ImageUtils.loadTexture(urls[i], {},function() { }), 
                    map: texture, 
                    overdraw: true,
                    side: THREE.BackSide,
                    //transparent: true,
                    //needsUpdate:true,
                    premultipliedAlpha: true
                    //depthWrite:true,
                    
    //              wireframe:false,
                })
                ); 
                
            } 
            
            var cube = new THREE.Mesh(new THREE.CubeGeometry(4000, 4000,4000), new THREE.MeshFaceMaterial(skyMaterials)); 
            cube.name="sky";
            scene.add(cube);
            
            createMtlObj({
                mtlBaseUrl:"../resource/haven/",
                mtlPath: "../resource/haven/",
                mtlFileName:"threejs.mtl",
                objPath:"../resource/haven/",
                objFileName:"threejs.obj",
                completeCallback:function(object){
                    object.traverse(function(child) { 
                        if (child instanceof THREE.Mesh) { 
                            child.material.side = THREE.DoubleSide;
                            child.material.emissive.r=0;
                            child.material.emissive.g=0.01;
                            child.material.emissive.b=0.05;
                            child.material.transparent=true;
                            //child.material.opacity=0;                     
                            //child.material.shading=THREE.SmoothShading;
                        }
                    });

                    object.emissive=0x00ffff;
                    object.ambient=0x00ffff;
                    //object.rotation.x= 10/180*Math.PI;
                    object.position.y = 0;
                    object.position.z = 0;
                    object.scale.x=1;
                    object.scale.y=1;
                    object.scale.z=1;
                    object.name="haven";
                    object.rotation.y=-Math.PI;
                    scene.add(object);
                },
                progress:function(persent){
                    
                    $("#havenloading .progress").css("width",persent+"%");
                }
            })
            renderer = new THREE.WebGLRenderer();
            renderer.setPixelRatio( window.devicePixelRatio );
            renderer.setSize( window.innerWidth, window.innerHeight );
            container.appendChild( renderer.domElement );
            document.addEventListener( 'mousemove', onDocumentMouseMove, false );
            window.addEventListener( 'resize', onWindowResize, false );
        }
        
        var controls = new THREE.OrbitControls(camera,container);
        //controls.maxPolarAngle=1.5;
        //controls.minPolarAngle=1;
        controls.enableDamping=true;
        controls.enableKeys=false;
        controls.enablePan=false;
        controls.dampingFactor = 0.1;
        controls.rotateSpeed=0.1;
//      controls.enabled = false;
        //controls.minDistance=1000;
        //controls.maxDistance=3000;
        function createMtlObj(options){
        //      options={
        //          mtlBaseUrl:"",
        //          mtlPath:"",
        //          mtlFileName:"",
        //          objPath:"",
        //          objFileName:"",
        //          completeCallback:function(object){  
        //          }
        //          progress:function(persent){
        //              
        //          }
        //      }
                //THREE.Loader.Handlers.add( /\.dds$/i, new THREE.DDSLoader() );
            var mtlLoader = new THREE.MTLLoader();
            mtlLoader.setBaseUrl( options.mtlBaseUrl );
            mtlLoader.setPath( options.mtlPath );
            mtlLoader.load( options.mtlFileName, function( materials ) {
                materials.preload();
                var objLoader = new THREE.OBJLoader();
                objLoader.setMaterials( materials );
                objLoader.setPath( options.objPath );
                objLoader.load( options.objFileName, function ( object ) {
                    if(typeof options.completeCallback=="function"){
                        options.completeCallback(object);
                    }
                }, function ( xhr ) {
                    if ( xhr.lengthComputable ) {
                        var percentComplete = xhr.loaded / xhr.total * 100;
                        if(typeof options.progress =="function"){
                            options.progress( Math.round(percentComplete, 2));
                        }
                        //console.log( Math.round(percentComplete, 2) + '% downloaded' );
                    }
                }, function(error){
                    
                } );
        
            });
        }
        function onWindowResize() {
            windowHalfX = window.innerWidth / 2;
            windowHalfY = window.innerHeight / 2;
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize( window.innerWidth, window.innerHeight );
        }
        function onDocumentMouseMove( event ) {
            mouseX = ( event.clientX - windowHalfX ) / 2;
            mouseY = ( event.clientY - windowHalfY ) / 2;
        }
        function animate() {
            requestAnimationFrame( animate );
            render();
        }
        function render() {
//              camera.position.x += ( mouseX - camera.position.x ) ;
//              camera.position.y += ( mouseY - camera.position.y ) ;
            camera.lookAt( scene.position );
            renderer.render( scene, camera );

        }
        
    </script>
    </body>
</html>


使用jsonLoader加载3D模型

threejs提供了对json格式的支持,相对obj格式的文件来说要小很多,可以提高模型的加载速度。
将obj文件转为了json格式需要借助一个软件:blender以及THREEJS提供的插件io_three;
软件下载地址:https://www.blender.org/
io_three插件在three代码包里的路径:three.js-master/utils/exporters/blender/addons

安装插件的方法

  • 将io_three 复制到blender安装目录下的addons目录中,如:D:\blender-2.77a-windows64\2.77\scripts\addons
  • 打开blender,点击菜单->用户设置,向下拖动保存用户设置按钮所在的区域,会显示所有的用户设置内容
  • 选择add-ons选项卡,在左侧搜索框输入three ,将右侧出现的结果勾选上,到此则已经启用该插件。

1470192336842.png

转换格式

  • 在blender中新建 一个项目,删除默认添加的正方体等对象
  • 切换回info菜单栏,点击文件->导入->*.obj,选择obj之后,blender中会显示模型
  • 点击文件->导出->Threejs(.json),保存为haven.json,存储到材质所在的路径。导出设置如下:

1470209341377.png

使用JsonLoader载入json

var loader = new THREE.JSONLoader();
loader.load( '../resource/haven/haven.json', function ( geometry, materials ) {
    var object = new THREE.Mesh( geometry, materials[0] );
    object.material.side = THREE.DoubleSide;
    object.material.emissive.r=0;
    object.material.emissive.g=0.01;
    object.material.emissive.b=0.05;
    object.material.transparent=true;
    //object.material.opacity=0;                        
    object.material.shading=THREE.SmoothShading;
    object.position.y = 0;
    object.position.z = 0;
    object.scale.x=1;
    object.scale.y=1;
    object.scale.z=1;
    scene.add(object);
})

这时我们看到的图像是这样的

1470209408071.png

是的,没错,blender只能将当前选中的单个模型导出json文件,我们使用的obj文件是由多个子模型构成的模型组合,需要对每个子模型单独导出为json文件,并在页面中使用jsonLoader单独载入。

手机阅读请扫描下方二维码:

已有 24 条评论

  1. 有在线预览地址吗? 我教程走 根本没用啊……
    报错
    Uncaught TypeError: Cannot read property 'visible' of undefined
    at p (three.min.js:134)
    at p (three.min.js:134)
    at Xd.render (three.min.js:173)
    at render (main.js?v=fsd:195)
    at animate (main.js?v=fsd:189)

    1. ubee

      我的代码里没有设置visible属性的,看一下是否你自己的代码里main属性里的animate方法里的脚本是否有多余的设置

  2. h.g.

    咨询一个问题,向blender导入文件的时候,为什么就只导入模型,没有图片。这样模型没有皮肤。很奇怪。

  3. [...]http://feg.netease.com/archives/301.html[...]

  4. test

  5. bjzhangzhilei

    你好,我是北京网易传媒的,有个3dmax方面的问题请教一下,你们3dmax贴图的材质球是用的默认扫描线渲染器下的材质球吗?但是看你们的mtl文件中的代码,是newmtl,这个应该是mental rey或v-rey渲染器下的名字,我用这两个渲染器下的材质球,导出obj时,贴图导不出来,只能在默认扫描线渲染器下的材质球导出obj才能导出贴图。这是我遇到的问题以及总结http://jingyan.baidu.com/article/fec7a1e5d9e7011190b4e7b6.html,我觉得您是用插件渲染器导出的贴图,求指教,谢谢啦!

    1. bjzhangzhilei

      找到答案了,已经解决!

      1. ubee

        3Dmax是由美术同学提供的。具体用哪个渲染器我就不知道了

      2. yang61591

        找到什么答案可以分享么~最近也踩了你说的坑 ali-43.gif

      3. JsonDiv

        天啦噜,找到答案了也不说一下,还出来炫耀一下。。。 ali-45.gif

    2. ubee

      这个问题,obj本身不带贴图,需要mtl同时导入,另外导出obj+mtl时,如果贴图路径没设置正确,可以用文本编辑器打开mtl文件,把图片路径修改为项目的相对路径。
      单独导入obj是没有贴图的。如果只有obj可以用threejs的textureload导入贴图,并在导入obj后, 把obj的mesh.materital设置为导入的贴图。

      1. zm10

        您好,我导出obj格式后,没有导出贴图文件,mtl文件中也没有贴图路径是为什么呢

  6. ubee

    我的代码里没有设置visible属性的,看一下是否你自己的代码里main属性里的animate方法里的脚本是否有多余的设置

  7. 翟浩

    能否给demo包,新手求老司机领路 ali-52.gif

    1. ubee

      直接上threejs.org上找就可以了
      加载obj的是这个 demo
      https://threejs.org/examples/?q=obj#webgl_loader_obj_mtl

  8. leovan

    能否给个demo包,新手求老司机带路

    1. ubee

      直接上threejs.org上找就可以了
      加载obj的是这个 demo
      https://threejs.org/examples/?q=obj#webgl_loader_obj_mtl

  9. inuyasha1981

    想请教一个问题,three.js 导入 Obj 时,如果 Obj 本身是带有分组的,会把分组下的原件重复添加进去。比如在3D建模中,Room作为一个模型,编组下面有 Desk 对象。那么 three.js 导入 obj 后,Desk 这个对象会被导入两次,生成一个 Desk_1。这个怎么解决?如果不在建模时分组,那么我又怎么样在导入 Obj 之后把 Desk 和 Room 重新编组呢?例如 Room.add(Desk); 这样的形式。希望有时间的话可以回复,谢谢。

    1. ubee

      你这个room应该只是一个group,你可以只循环mesh添加,或者只添加group,这样就只会导入一次,重新编组,可以var g=new THREE.Group(); g.add(mesh1);g.add(mesh2),这样子,mesh1 和mesh2的position参考系就以g的中心点为原点。

  10. zhang

    问一下,max2012导出obj包含贴图并转换格式之后,如dds转tga。dds转xxx,导出的贴图都变暗了。用2010的倒是没问题。另外 使用obj导出的贴图不能带通道吗?源贴图是有通道的,导出之后通道就消失了? ali-45.gif

    1. ubee

      我之前也遇过这个问题,后来发现threejs只贴了一个通道的图,最后我只能把所有的通道都合并成一张图,或者使用lightMAP来贴光影图。

    2. ubee

      用ligthMAP还需要把UV展开,搞2层uv,在原来的uv上贴上默认贴图,在uv2上贴光影图。而obj不支持2UV,只能blender加载包含2uv的模型,然后导出带2uv信息的json,再用threejs导入json。

  11. 不吃火锅底料

    你好,请问下如果我要加载不同的obj模型,建模出来的obj模型大小位置都不统一,这时候不能把camera的位置写死吧,该如何做到设置camera位置适配不同的模型啊

    1. ubee

      建议调整模型的position 和scale 来统一,而不调整camera。调camera的位置更难统一视觉效果。。

添加新评论

ali-40.gifali-41.gifali-42.gifali-43.gifali-44.gifali-45.gifali-46.gifali-47.gifali-48.gifali-49.gifali-50.gifali-51.gifali-52.gifali-53.gifali-54.gifali-55.gifali-56.gifali-57.gifali-58.gifali-59.gifali-60.gifali-61.gif