回顧與開始
在上一篇學(xué)習(xí)筆記《Three基礎(chǔ)使用:一篇就會(huì)!》中,我們已經(jīng)學(xué)會(huì)了如何創(chuàng)建一個(gè)場(chǎng)景并通過相機(jī)觀察它,同時(shí)我們還學(xué)習(xí)了一些three的相關(guān)概念,例如:場(chǎng)景、相機(jī)、渲染器等等。
流程走通了,但是相比沒有人會(huì)滿足與看一個(gè)不停旋轉(zhuǎn)的立方體~這一篇,我們就一起來研究如何導(dǎo)入一個(gè)3d模型
模型從哪兒來
可以從網(wǎng)上下載,也可以從blender等3d建模軟件中建模后導(dǎo)出。
three支持的3d模型的格式有多種,這里我們以gltf格式的模型為例
原理概述
three可用需要幾步:
- 有畫布div
- 有場(chǎng)景scene
- 有光照l(shuí)ight
- 有相機(jī)camera
- 有渲染器renderer——循環(huán)渲染
導(dǎo)入模型意思就是把scene替換成外部引入的模型
開始
創(chuàng)建畫布
render() {
return (
<div id='l-canvas-frame' style={{ backgroundColor: "yellowgreen" }}>
</div>
);
}
畫布作為容器,場(chǎng)景的起始位置與畫布有關(guān),但是畫布并不能限制場(chǎng)景的大小。
創(chuàng)建渲染器
const renderer=this.rendererinit(canvasId,rendererwidth,rendererheight,);
rendererinit(canvasId,width,height) {
const render = new THREE.WebGLRenderer({
antialias: true
});//調(diào)用THREE里面的方法創(chuàng)建渲染器
render.setSize(width, height); //設(shè)置渲染器的寬和高,這個(gè)真正能夠影響場(chǎng)景的大小
document.getElementById(canvasId).appendChild(render.domElement);
//把渲染器添加為畫布的子元素,這樣渲染的效果就會(huì)展示在畫布容器中
render.setClearColor(0xffffff, 1.0);//渲染器中沒有模型的地方的顏色
return render
}
創(chuàng)建場(chǎng)景:這是本章重點(diǎn),導(dǎo)入網(wǎng)格對(duì)象!
let scene=this.initScene(ifShowAxes);
initScene(ifShowAxes) {
const scene = new THREE.Scene(); //調(diào)用THREE的方法創(chuàng)建一個(gè)場(chǎng)景
//下面是顯示坐標(biāo)軸的函數(shù),非必須
if(ifShowAxes){
var axisHelper = new THREE.AxesHelper(250);
scene.add(axisHelper);
}
return scene
}
向場(chǎng)景中添加物體
scene=this.initObject(url,scene); //這里的url是需要導(dǎo)入的模型的地址
- 導(dǎo)入網(wǎng)格對(duì)象
//這里以gltf模型為例,THREE不僅僅可以展示gltf模型
initObject(url,scene) {
const loader = new GLTFLoader(); //使用THREE自帶的GLTFLoader去加載gltf模型
loader.load(url, function (gltf) {
// .load方法的第一個(gè)參數(shù)是url,第二三四個(gè)參數(shù)分別是載入成功、載入中、載入失敗的回調(diào)函數(shù)
// 返回的場(chǎng)景對(duì)象gltf.scene插入到threejs場(chǎng)景中
gltf.scene.position.x = 100
gltf.scene.position.z = -850
scene.add(gltf.scene);
})
return scene
}
- 自己使用THREE創(chuàng)建網(wǎng)格對(duì)象
initObject(url,scene) {
const geometry =new THREE.BoxGeometry() //創(chuàng)建一個(gè)正方體模型
const mesh = new THREE.MeshLambertMaterial({color:0x00ff00})
//給模型加上材質(zhì):這里是用來設(shè)置其顏色為綠色
const cube = new THREE.Mesh(geometry,mesh) //模型+材質(zhì)=網(wǎng)格對(duì)象
const edgeMaterial = new THREE.LineBasicMaterial({color:0xff0000,linewidth:5})
//創(chuàng)建線的某種材質(zhì)。用于設(shè)置線條顏色和寬度
const edges = new THREE.EdgesGeometry(geometry)
//創(chuàng)建一個(gè)線條模型對(duì)象,這一這里傳入了參數(shù)geometry,
// 這個(gè)方法為模型geometry創(chuàng)建了一個(gè)棱線框,這決定了線條對(duì)象的形狀
const line = new THREE.LineSegments(edges,edgeMaterial)
//利用線的模型對(duì)象及其材質(zhì)生成一個(gè)網(wǎng)格對(duì)象
scene.add(cube) //將正方體網(wǎng)格對(duì)象添加進(jìn)場(chǎng)景
scene.add(line) //將線框的方格對(duì)象添加進(jìn)場(chǎng)景
return scene
}

紅色的就是注釋中提到的線框
向場(chǎng)景中添加燈光
光線的種類很多

- 注意有些模型材質(zhì),例如金屬,表面足夠光滑是沒有漫反射效果的,所以Three里面的某些光線,如環(huán)境光是不生效的
- 下面以平行光(THREE.DirectionalLight)為例,從上左右前后五個(gè)方向進(jìn)行打光
scene=this.initLight(scene);
initLight(scene) {
//FIXME: 實(shí)現(xiàn)自定義的燈光。根據(jù)傳參數(shù)組,遍歷以生成相應(yīng)的燈光
const lightIntensity=2.6
const lightColor1=0xf0f0f0
const directionalLight = new THREE.DirectionalLight(lightColor1, lightIntensity);
//平行光:第一個(gè)參數(shù)是光的顏色,第二個(gè)參數(shù)是光的強(qiáng)度,默認(rèn)為1,但是可以大于1
directionalLight.position.set(0,1,0);//設(shè)置平行光照射的方向,這里表示順著y軸向負(fù)方向照射
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
}
創(chuàng)建相機(jī)
從哪兒看?睜開多大的眼睛看?怎么看?睜?zhēng)字谎劬矗?br> 相機(jī)也有很多種:陣列相機(jī)、抽象相機(jī)基類、立方相機(jī)、正交、立體、透視相機(jī)

下面的例子中用的是正交相機(jī)
const camera=this.initCamera(cameraWidth,cameraHeight,scale,cameraPos,cameraLookPos,scene);
initCamera(width,height,scale,cameraPos,cameraLookPos,scene) {
const k = width / height; //窗口寬高比
const s=scale//三維場(chǎng)景顯示范圍控制系數(shù),系數(shù)越大,顯示的范圍越大
//創(chuàng)建相機(jī)對(duì)象,六個(gè)參數(shù)依次為左右上下近遠(yuǎn)的最大可視距離
const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(cameraPos.x, cameraPos.y, cameraPos.z); //設(shè)置相機(jī)位置
if(cameraLookPos){
//設(shè)置相機(jī)看向哪個(gè)點(diǎn)
camera.lookAt(cameraLookPos.x,cameraLookPos.y,cameraLookPos.z)
}
else{
//設(shè)置坐標(biāo)看向場(chǎng)景位置,
camera.lookAt(scene.position); //設(shè)置相機(jī)方向(指向的場(chǎng)景對(duì)象)
}
return camera
}
使用渲染器將場(chǎng)景和相機(jī)鏈接起來
renderer.render(scene, camera);
但是為了當(dāng)場(chǎng)景變化時(shí)我們能夠反復(fù)地去渲染畫面,需要調(diào)用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:
* 對(duì)于燈光,目前設(shè)置的是固定的,后續(xù)需要開放這一塊的靈活性,思路是設(shè)置為數(shù)組對(duì)象的形式接收參數(shù),遍歷以設(shè)置燈光
* 對(duì)于模型的url,目前是固定的,因?yàn)椴惶宄@個(gè)路徑是怎么檢測(cè),后面改掉
*/
/**USEAGE: 本組件傳參:
* canvasId,
rendererwidth, 渲染器的寬高:這個(gè)是真正影響到我們看到的場(chǎng)景的大小的。畫布并不能限制場(chǎng)景的大小
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,);
//初始化場(chǎng)景
let scene=this.initScene(ifShowAxes);
//初始化相機(jī)
const camera=this.initCamera(cameraWidth,cameraHeight,scale,cameraPos,cameraLookPos,scene);
//初始化燈光并將燈光加入scene------------這個(gè)后續(xù)需要開放更多靈活性
scene=this.initLight(scene);
//初始化object并將object加入到scene——在這一步導(dǎo)入模型:當(dāng)前只支持gltf
scene=this.initObject(url,scene);
/**
* NOTE:
* 下面必須這樣寫,不能通過傳參的形式去執(zhí)行animation,目的是要改變r(jià)enderer的本體,而非傳參得到的復(fù)制體
*/
function adnimation(){
renderer.render(scene, camera);
requestAnimationFrame(adnimation);
}
adnimation()
/**
* NOTE:
* 下面這個(gè)是必須要實(shí)例化的,用作模型的控制器,鼠標(biāo)才能對(duì)模型進(jìn)行拖動(dòng)
*/
const controls = new OrbitControls(camera, renderer.domElement);//創(chuàng)建控件對(duì)象
}
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//三維場(chǎng)景顯示范圍控制系數(shù),系數(shù)越大,顯示的范圍越大
//創(chuàng)建相機(jī)對(duì)象
const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
camera.position.set(cameraPos.x, cameraPos.y, cameraPos.z); //設(shè)置相機(jī)位置
if(cameraLookPos){
camera.lookAt(cameraLookPos.x,cameraLookPos.y,cameraLookPos.z)
}
else{
camera.lookAt(scene.position); //設(shè)置相機(jī)方向(指向的場(chǎng)景對(duì)象)
}
return camera
}
initLight(scene) {
//FIXME: 實(shí)現(xiàn)自定義的燈光。根據(jù)傳參數(shù)組,遍歷以生成相應(yīng)的燈光
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); //點(diǎn)光源位置
// scene.add(point); //點(diǎn)光源添加到場(chǎng)景中
// const ambient = new THREE.AmbientLight(0xFFFFFF,100);//環(huán)境光
// scene.add(ambient);
return scene
}
initObject(url,scene) {
const loader = new GLTFLoader();
loader.load(url, function (gltf) {
// 返回的場(chǎng)景對(duì)象gltf.scene插入到threejs場(chǎng)景中
gltf.scene.position.x = 100
gltf.scene.position.z = -850
//FIXME: 這里需要我自由替換導(dǎo)入的gltf的材質(zhì),純金屬材質(zhì)并不好看
// var model = gltf.scene;
// var newMaterial = new THREE.MeshStandardMaterial({color: 0xff0000});
// model.traverse((o) => {
// if (o.isMesh) o.material = newMaterial;
// });
//FIXME: 弄清楚引用組件時(shí),url的路徑是如何計(jì)算的
scene.add(gltf.scene);
},
function(e){
// console.log("正在導(dǎo)入",e);
},
function(e){
console.log("導(dǎo)入失敗",e);
})
return scene
}
/**
* 開始Three
*
* @memberof LoadModel
*/
componentDidMount() {
const {canvasId,rendererwidth,rendererheight,cameraWidth,cameraHeight,scale,cameraPos,cameraLookPos,ifShowAxes,url}=this.props
const param={
//畫布相關(guān)參數(shù)
canvasId,
rendererwidth:rendererwidth||window.innerWidth,
rendererheight:rendererheight||window.innerHeight,
//相機(jī)相關(guān)參數(shù)
cameraWidth:cameraWidth||window.innerWidth,
cameraHeight:cameraHeight||window.innerHeight,
scale:scale||100,
cameraPos:cameraPos||{x:500,y:500,z:500},
cameraLookPos:cameraLookPos,
//場(chǎng)景相關(guān)參數(shù)
ifShowAxes:ifShowAxes||true,
url,//模型的url
}
this.threeStart(param);
}
render() {
const {canvasId}=this.props
return (
<div id={canvasId} style={{ backgroundColor: "yellowgreen" }}>
</div>
);
}
}
export default LoadModel;