回顾与开始
在上一篇学习笔记《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;