Three模型导入——初尝

回顾与开始

在上一篇学习笔记《Three基础使用:一篇就会!》中,我们已经学会了如何创建一个场景并通过相机观察它,同时我们还学习了一些three的相关概念,例如:场景、相机、渲染器等等。
流程走通了,但是相比没有人会满足与看一个不停旋转的立方体~这一篇,我们就一起来研究如何导入一个3d模型

模型从哪儿来

可以从网上下载,也可以从blender等3d建模软件中建模后导出。
three支持的3d模型的格式有多种,这里我们以gltf格式的模型为例

原理概述

three可用需要几步:

  • 有画布div
  • 有场景scene
  • 有光照light
  • 有相机camera
  • 有渲染器renderer——循环渲染

导入模型意思就是把scene替换成外部引入的模型

开始

创建画布

render() {
        return (
            <div id='l-canvas-frame' style={{ backgroundColor: "yellowgreen" }}>
            </div>
        );
    }

画布作为容器,场景的起始位置与画布有关,但是画布并不能限制场景的大小。

创建渲染器

const renderer=this.rendererinit(canvasId,rendererwidth,rendererheight,);
rendererinit(canvasId,width,height) {
        const render = new THREE.WebGLRenderer({
            antialias: true
        });//调用THREE里面的方法创建渲染器
        render.setSize(width, height); //设置渲染器的宽和高,这个真正能够影响场景的大小
        document.getElementById(canvasId).appendChild(render.domElement);
        //把渲染器添加为画布的子元素,这样渲染的效果就会展示在画布容器中
        render.setClearColor(0xffffff, 1.0);//渲染器中没有模型的地方的颜色
        return render
    }

创建场景:这是本章重点,导入网格对象!

let scene=this.initScene(ifShowAxes);
initScene(ifShowAxes) {
        const scene = new THREE.Scene();  //调用THREE的方法创建一个场景
        //下面是显示坐标轴的函数,非必须
        if(ifShowAxes){
            var axisHelper = new THREE.AxesHelper(250);
            scene.add(axisHelper);
        }
        return scene
    }
向场景中添加物体
scene=this.initObject(url,scene);    //这里的url是需要导入的模型的地址
  • 导入网格对象
//这里以gltf模型为例,THREE不仅仅可以展示gltf模型
initObject(url,scene) {
        const loader = new GLTFLoader();  //使用THREE自带的GLTFLoader去加载gltf模型
        loader.load(url, function (gltf) {  
        // .load方法的第一个参数是url,第二三四个参数分别是载入成功、载入中、载入失败的回调函数
            // 返回的场景对象gltf.scene插入到threejs场景中
            gltf.scene.position.x = 100
            gltf.scene.position.z = -850
            scene.add(gltf.scene);
        })
        return scene
    }
  • 自己使用THREE创建网格对象
initObject(url,scene) {
        const geometry =new THREE.BoxGeometry()  //创建一个正方体模型
        const mesh = new THREE.MeshLambertMaterial({color:0x00ff00}) 
        //给模型加上材质:这里是用来设置其颜色为绿色
        const cube = new THREE.Mesh(geometry,mesh) //模型+材质=网格对象

        const edgeMaterial = new THREE.LineBasicMaterial({color:0xff0000,linewidth:5}) 
        //创建线的某种材质。用于设置线条颜色和宽度
        const edges = new THREE.EdgesGeometry(geometry) 
        //创建一个线条模型对象,这一这里传入了参数geometry,
        // 这个方法为模型geometry创建了一个棱线框,这决定了线条对象的形状
        const line = new THREE.LineSegments(edges,edgeMaterial)  
        //利用线的模型对象及其材质生成一个网格对象

        scene.add(cube)  //将正方体网格对象添加进场景
        scene.add(line)  //将线框的方格对象添加进场景
        return scene
    }

红色的就是注释中提到的线框

向场景中添加灯光

光线的种类很多


  • 注意有些模型材质,例如金属,表面足够光滑是没有漫反射效果的,所以Three里面的某些光线,如环境光是不生效的
  • 下面以平行光(THREE.DirectionalLight)为例,从上左右前后五个方向进行打光
scene=this.initLight(scene);
initLight(scene) {
        //FIXME: 实现自定义的灯光。根据传参数组,遍历以生成相应的灯光
        const lightIntensity=2.6
        const lightColor1=0xf0f0f0
        const directionalLight = new THREE.DirectionalLight(lightColor1, lightIntensity);
        //平行光:第一个参数是光的颜色,第二个参数是光的强度,默认为1,但是可以大于1
        directionalLight.position.set(0,1,0);//设置平行光照射的方向,这里表示顺着y轴向负方向照射
        scene.add(directionalLight);
        const directionalLight1 = new THREE.DirectionalLight(lightColor1, lightIntensity);//平行光
        directionalLight1.position.set(1,0,0);
        scene.add(directionalLight1);
        const directionalLight2 = new THREE.DirectionalLight(lightColor1, lightIntensity);//平行光
        directionalLight2.position.set(-1,0,0);
        scene.add(directionalLight2);
        const directionalLight3 = new THREE.DirectionalLight(lightColor1, lightIntensity);//平行光
        directionalLight3.position.set(0,0,1);
        scene.add(directionalLight3);
        const directionalLight4 = new THREE.DirectionalLight(lightColor1, lightIntensity);//平行光
        directionalLight4.position.set(0,0,-1);
        scene.add(directionalLight4);

        return scene
    }

创建相机

从哪儿看?睁开多大的眼睛看?怎么看?睁几只眼睛看?
相机也有很多种:阵列相机、抽象相机基类、立方相机、正交、立体、透视相机



下面的例子中用的是正交相机

const camera=this.initCamera(cameraWidth,cameraHeight,scale,cameraPos,cameraLookPos,scene);
initCamera(width,height,scale,cameraPos,cameraLookPos,scene) {
        const k = width / height; //窗口宽高比
        const s=scale//三维场景显示范围控制系数,系数越大,显示的范围越大
        //创建相机对象,六个参数依次为左右上下近远的最大可视距离
        const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
        camera.position.set(cameraPos.x, cameraPos.y, cameraPos.z); //设置相机位置
        if(cameraLookPos){
        //设置相机看向哪个点
            camera.lookAt(cameraLookPos.x,cameraLookPos.y,cameraLookPos.z)
        }
        else{
        //设置坐标看向场景位置,
            camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
        }
        return camera
    }

使用渲染器将场景和相机链接起来

renderer.render(scene, camera);
但是为了当场景变化时我们能够反复地去渲染画面,需要调用requestAnimationFrame方法,完整写法是这样的:

        function adnimation(){
            renderer.render(scene, camera);
            requestAnimationFrame(adnimation);
        }

        adnimation()

完整代码如下:

已封装为react组件

//TITLE: 本组件用于gltf模型的载入,单纯用于展示
import React, { Component } from 'react';
import * as THREE from 'three';
import { Loader } from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

/**TODO:
 * 对于灯光,目前设置的是固定的,后续需要开放这一块的灵活性,思路是设置为数组对象的形式接收参数,遍历以设置灯光
 * 对于模型的url,目前是固定的,因为不太清楚这个路径是怎么检测,后面改掉
 */

/**USEAGE: 本组件传参:
 *  canvasId,           
    rendererwidth,  渲染器的宽高:这个是真正影响到我们看到的场景的大小的。画布并不能限制场景的大小
    rendererheight,


    cameraWidth,
    cameraHeight,
    scale,
    cameraPos,
    cameraLookPos,
    ifShowAxes,
    url
 */

class LoadModel extends Component {
    constructor(props) {
        super(props);
        this.state = {};
    }

    threeStart(param) {
        const {canvasId,
            rendererwidth,
            rendererheight,
            cameraWidth,
            cameraHeight,
            scale,
            cameraPos,
            cameraLookPos,
            ifShowAxes,
            url,
            
        }=param

        //初始化渲染器
        const renderer=this.rendererinit(canvasId,rendererwidth,rendererheight,);
        //初始化场景
        let scene=this.initScene(ifShowAxes);
        //初始化相机
        const camera=this.initCamera(cameraWidth,cameraHeight,scale,cameraPos,cameraLookPos,scene);
        //初始化灯光并将灯光加入scene------------这个后续需要开放更多灵活性
        scene=this.initLight(scene);
        //初始化object并将object加入到scene——在这一步导入模型:当前只支持gltf
        scene=this.initObject(url,scene);
        /**
         * NOTE:
         * 下面必须这样写,不能通过传参的形式去执行animation,目的是要改变renderer的本体,而非传参得到的复制体
         */
        function adnimation(){
            renderer.render(scene, camera);
            requestAnimationFrame(adnimation);
        }
        adnimation()
        
        

        /**
         * NOTE:
         * 下面这个是必须要实例化的,用作模型的控制器,鼠标才能对模型进行拖动
         */
        const controls = new OrbitControls(camera, renderer.domElement);//创建控件对象

    }

    rendererinit(canvasId,width,height) {
        const render = new THREE.WebGLRenderer({
            antialias: true
        });
        render.setSize(width, height);
        document.getElementById(canvasId).appendChild(render.domElement);
        render.setClearColor(0xffffff, 1.0);
        return render
    }

    initScene(ifShowAxes) {
        const scene = new THREE.Scene();
        if(ifShowAxes){
            var axisHelper = new THREE.AxesHelper(250);
            scene.add(axisHelper);
        }
        

        return scene
    }

    initCamera(width,height,scale,cameraPos,cameraLookPos,scene) {
        const k = width / height; //窗口宽高比
        const s=scale//三维场景显示范围控制系数,系数越大,显示的范围越大
        //创建相机对象
        const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
        camera.position.set(cameraPos.x, cameraPos.y, cameraPos.z); //设置相机位置

        if(cameraLookPos){
            camera.lookAt(cameraLookPos.x,cameraLookPos.y,cameraLookPos.z)
        }
        else{
            camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
        }
        

        return camera
    }

    initLight(scene) {
        //FIXME: 实现自定义的灯光。根据传参数组,遍历以生成相应的灯光
        const lightIntensity=2.6
        const lightColor1=0xf0f0f0
        const directionalLight = new THREE.DirectionalLight(lightColor1, lightIntensity);//平行光
        directionalLight.position.set(0,1,0);
        scene.add(directionalLight);
        // const directionalLight0 = new THREE.DirectionalLight(lightColor1, lightIntensity);//平行光
        // directionalLight0.position.set(0,-1,0);
        // scene.add(directionalLight0);
        const directionalLight1 = new THREE.DirectionalLight(lightColor1, lightIntensity);//平行光
        directionalLight1.position.set(1,0,0);
        scene.add(directionalLight1);
        const directionalLight2 = new THREE.DirectionalLight(lightColor1, lightIntensity);//平行光
        directionalLight2.position.set(-1,0,0);
        scene.add(directionalLight2);
        const directionalLight3 = new THREE.DirectionalLight(lightColor1, lightIntensity);//平行光
        directionalLight3.position.set(0,0,1);
        scene.add(directionalLight3);
        const directionalLight4 = new THREE.DirectionalLight(lightColor1, lightIntensity);//平行光
        directionalLight4.position.set(0,0,-1);
        scene.add(directionalLight4);

        // const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 200);//半球光
        // hemisphereLight.position.set(0, 0, 0);
        // scene.add(hemisphereLight);
    
        // const point = new THREE.PointLight(0xFFFFF5);
        // point.position.set(300, 300, 300); //点光源位置
        // scene.add(point); //点光源添加到场景中

        // const ambient = new THREE.AmbientLight(0xFFFFFF,100);//环境光
        // scene.add(ambient);
        return scene
    }

    initObject(url,scene) {


        const loader = new GLTFLoader();
        loader.load(url, function (gltf) {
            // 返回的场景对象gltf.scene插入到threejs场景中
            gltf.scene.position.x = 100
            gltf.scene.position.z = -850

            //FIXME: 这里需要我自由替换导入的gltf的材质,纯金属材质并不好看
            // var model = gltf.scene;
            // var newMaterial = new THREE.MeshStandardMaterial({color: 0xff0000});
            // model.traverse((o) => {
            // if (o.isMesh) o.material = newMaterial;
            // });
        
            //FIXME: 弄清楚引用组件时,url的路径是如何计算的

            scene.add(gltf.scene);
        },
        function(e){
            // console.log("正在导入",e);
        },
        function(e){
            console.log("导入失败",e);
        })
        return scene
    }





    /**
     * 开始Three
     *
     * @memberof LoadModel
     */
    componentDidMount() {

        const {canvasId,rendererwidth,rendererheight,cameraWidth,cameraHeight,scale,cameraPos,cameraLookPos,ifShowAxes,url}=this.props
        const param={
            //画布相关参数
            canvasId,
            rendererwidth:rendererwidth||window.innerWidth,
            rendererheight:rendererheight||window.innerHeight,

            //相机相关参数
            cameraWidth:cameraWidth||window.innerWidth,
            cameraHeight:cameraHeight||window.innerHeight,
            scale:scale||100,
            cameraPos:cameraPos||{x:500,y:500,z:500},
            cameraLookPos:cameraLookPos,
            //场景相关参数
            ifShowAxes:ifShowAxes||true,
            url,//模型的url

        }

        this.threeStart(param);
    }
    render() {
        const {canvasId}=this.props
        return (
            <div id={canvasId} style={{ backgroundColor: "yellowgreen" }}>
            </div>
        );
    }
}

export default LoadModel;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容