github 的地址 歡迎 star!
javascript
以前項目中用到了 3D 模型演示的問題,整理了一下以前學習總結以及遇到的坑。3D 框架有老牌引擎 Three.js 和微軟的 Babylon.js php
主要來自於《Three.js 開發指南》也能夠參考在線網站 threejs 教程 css
Sence 場景:場景是一個載體,容器,全部的一切都運行在這個容器裏面(存放着全部渲染的物體和使用的光源)html
相機 camera 的做用是定義可視域,至關於咱們的雙眼,生產一個個快照,最爲經常使用的是 PerspectiveCamera 透視攝像機,其餘還有 ArrayCamera 陣列攝像機(包含多個子攝像機,經過這一組子攝像機渲染出實際效果,適用於 VR 場景),CubeCamera 立方攝像機(建立六個 PerspectiveCamera(透視攝像機),適用於鏡面場景),StereoCamera 立體相機(雙透視攝像機適用於 3D 影片、視差效果)。相機主要分爲兩類正投影相機和透視相機,正投影相機的話, 全部方塊渲染出來的尺寸都同樣; 對象和相機之間的距離不會影響渲染結果,而透視相機接近真實世界,看物體會產生遠近高低各不一樣前端
PerspectiveCamera 透視攝像機--模擬人眼的視覺,根據物體距離攝像機的距離,近大遠小html5
// 場景是全部物體的容器
var scene = new THREE.Scene();
// 相機,相機決定了場景中那個角度的景色會顯示出來。相機就像人的眼睛同樣,人站在不一樣位置,擡頭或者低頭都可以看到不一樣的景色。
var camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
// 渲染器renderer的domElement元素,表示渲染器中的畫布,全部的渲染都是畫在domElement上的
var renderer = new THREE.WebGLRenderer(); // 渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 設置渲染器的大小爲窗口的內寬度,也就是內容區的寬度
document.body.appendChild(renderer.domElement);
// 渲染循環
function animate() {
render();
// 調用 requestAnimationFrame 函數,傳遞一個 callback 參數,則在下一個動畫幀時,會調用 callback 這個函數。
requestAnimationFrame( animate );
}
動畫方案:
一:改變camera
function animation()
{
//renderer.clear();
camera.position.x =camera.position.x +1;
renderer.render(scene, camera);
requestAnimationFrame(animation);
}
// camera.position.x =camera.position.x +1;
// 將相機不斷的沿着x軸移動1個單位,也就是相機向右移動,那麼相機中物體是向左移動的。
// 調用requestAnimationFrame(animation)函數,這個函數又會在下一個動畫幀出發animation()函數,這樣就不斷改變了相機的位置,從而物體看上去在移動了。
// 另外,必需要重視render函數,這個函數是從新繪製渲染結果,若是不調用這個函數,那麼即便相機的位置變化了,可是沒有從新繪製,仍然顯示的是上一幀的動畫 renderer.render(scene, camera);
二:改變物體自身位置--mesh
mesh就是指的物體,它有一個位置屬性position,這個position是一個THREE.Vector3類型變量,因此你要把它向左移動,只須要將x的值不斷的減小就能夠了。這裏咱們減去的是1個單位。
// [渲染真實性---光源運用](http://www.hewebgl.com/article/getarticle/60)
THREE.Light ( hex )
它有一個參數hex,接受一個16進制的顏色值。例如要定義一種紅色的光源,咱們能夠這樣來定義:
Var redLight = new THREE.Light(0xFF0000);
// [文理--3D物體的皮膚:](http://www.hewebgl.com/article/getarticle/68)
紋理類由THREE.Texture表示,其構造函數以下所示:
THREE.Texture( image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy )
複製代碼
一下就是 Three.js 的基本概念 java
而後給出一個簡單的例子react
// 引入 Three.js 庫
<script src="https://unpkg.com/three"></script>
function init () {
// 獲取瀏覽器窗口的寬高,後續會用
var width = window.innerWidth
var height = window.innerHeight
// 建立一個場景
var scene = new THREE.Scene()
// 建立一個具備透視效果的攝像機
var camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 800)
// 設置攝像機位置,並將其朝向場景中心
camera.position.x = 10
camera.position.y = 10
camera.position.z = 30
camera.lookAt(scene.position)
// 建立一個 WebGL 渲染器,Three.js 還提供 <canvas>, <svg>, CSS3D 渲染器。
var renderer = new THREE.WebGLRenderer()
// 設置渲染器的清除顏色(即背景色)和尺寸。
// 若想用 body 做爲背景,則能夠不設置 clearColor,而後在建立渲染器時設置 alpha: true,即 new THREE.WebGLRenderer({ alpha: true })
renderer.setClearColor(0xffffff)
renderer.setSize(width, height)
// 建立一個長寬高均爲 4 個單位長度的立方體(幾何體)
var cubeGeometry = new THREE.BoxGeometry(4, 4, 4)
// 建立材質(該材質不受光源影響)
var cubeMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000
})
// 建立一個立方體網格(mesh):將材質包裹在幾何體上
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
// 設置網格的位置
cube.position.x = 0
cube.position.y = -2
cube.position.z = 0
// 將立方體網格加入到場景中
scene.add(cube)
// 將渲染器的輸出(此處是 canvas 元素)插入到 body 中
document.body.appendChild(renderer.domElement)
// 渲染,即攝像機拍下此刻的場景
renderer.render(scene, camera)
}
init()
複製代碼
在線的例子點擊webpack
主要是用來顯示性能幀數的git
FPS:最後一秒的幀數,越大越流暢
MS:渲染一幀須要的時間(毫秒),越低越好
MB:佔用的內存信息
CUSTOM:自定義面板
var stats = new Stats()
stats.showPanel(1)
document.body.appendChild(stats.dom)
function animate() {
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
複製代碼
能夠在 github.com/mrdoob/thre… 下載文件,查看\three.js-master\examples中例子熟悉相應的代碼
導入模型文件須要用到相應的 loader,經常使用 3d 軟件導出的格式,項目中主要是用了 OBJ 和 MTL 類型,OBJ 定義了幾何體,MTL 定義了材質
//當mtl中引用了dds類型的圖片時,還需導入DDSLoader文件。
//這裏的src路徑視實際開發而定
<script src="js/loaders/DDSLoader.js"></script>
<script src="js/loaders/MTLLoader.js"></script>
<script src="js/loaders/OBJLoader.js"></script>
THREE.Loader.Handlers.add( /\.dds$/i, new THREE.DDSLoader() );
var mtlLoader = new THREE.MTLLoader();
//設置路徑,也可不是設置,在load中加載完整路徑也可
mtlLoader.setPath( 'obj/male02/' );
mtlLoader.load( 'male02_dds.mtl',
// 資源加載成功後執行的函數
//@params materials THREE.MTLLoader.MaterialCreator
function( materials ) {
materials.preload();
var objLoader = new THREE.OBJLoader();
objLoader.setMaterials( materials );
objLoader.setPath( 'obj/male02/' );
objLoader.load( 'male02.obj', function ( object ) {
object.position.y = - 95;
scene.add( object );
});
});
複製代碼
具體例子能夠查看
.obj 是靜態模型,不支持動畫數據存儲,沒法使用模型的動畫,並且體積大, glTF 是由 Khronos Group 開發的 3D 模型文件格式,該格式的特色是最大程度的減小了 3D 模型文件的大小,提升了傳輸、加載以及解析 3D 模型文件的效率,而且它可擴展,可互操做。
.gltf 包含場景中節點層次結構、攝像機、網格、材質以及動畫等描述信息
Three.js 中使用 glTF 格式需額外引入 GLTFLoader.js 加載器。
var gltfLoader = new THREE.gltfLoader()
gltfLoader.load('./assets/box.gltf', function(sence) {
var object = scene.gltf // 模型對象
scene.add(object) // 將模型添加到場景中
})
複製代碼
glTF 模型中可使用 Blender 建模軟件製做動畫,導出後使用 GLTFLoader 加載到 Three.js 中,能夠拿到一個 animations 數組,animations 裏包含了模型的每一個動畫 Action 動做。
爲了獲取更好的網絡性能,還可使用 Draco工具進行壓縮,只有在模型文件不少時,才推薦壓縮(由於壓縮後格式改變,須要引入其餘的解析工具)
上面說到了動畫,關於動畫,能夠直接三方庫 Tween 動畫,在許同事提供的研究裏面有相關的運用。通常在 Three.js 動畫是使用 requestAnimationFrame(),當你須要更新屏幕畫面時就能夠調用此方法。在瀏覽器下次重繪前執行回調函數。回調的次數一般是每秒60次。
對模型實現淡入淡出、縮放、位移、旋轉等動畫推薦使用 GSAP 來實現更爲簡便。
let tween = new TimelineMax()
tween
.to(box.scale, 1, { // 從 1 縮放至 2,花費 1 秒
x: 2,
y: 2,
z: 2,
ease: Power0.easeInOut, // 速度曲線
onStart: function() {
// 監聽動畫開始
},
onUpdate: function() {
// 監聽動畫過程
},
onComplete: function() {
// 監聽動畫結束
}
})
.to(box.position, 1, { // 縮放結束後,位移 x 至 10,花費 1 秒
x: 10,
y: 0,
z: 0
})
複製代碼
場景控制器,OrbitControls 是用於調試 Camera 的方法,實例化後能夠經過鼠標拖拽來旋轉 Camera 鏡頭的角度,鼠標滾輪能夠控制 Camera 鏡頭的遠近距離,旋轉和遠近都會基於場景的中心點,在調試預覽則會輕鬆許多。
// 引入文件
<script src="js/OrbitControls.js"></script>
//場景控制器初始化
function initControls() {
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enabled = true; // 鼠標控制是否可用
// 是否自動旋轉
controls.autoRotate = true;
controls.autoRotateSpeed = 0.05;
//是否可旋轉,旋轉速度(鼠標左鍵)
controls.enableRotate = true;
controls.rotateSpeed = 0.3;
//controls.target = new THREE.Vector();//攝像機聚焦到某一個點
//最大最小相機移動距離(景深相機)
controls.minDistance = 10;
controls.maxDistance = 40;
//最大仰視角和俯視角
controls.minPolarAngle = Math.PI / 4; // 45度視角
controls.maxPolarAngle = Math.PI / 2.4; // 75度視角
//慣性滑動,滑動大小默認0.25
controls.enableDamping = true;
controls.dampingFactor = 0.25;
//是否可平移,默認移動速度爲7px
controls.enablePan = true;
controls.panSpeed = 0.5;
//controls.screenSpacePanning = true;
//滾輪縮放控制
controls.enableZoom = true;
controls.zoomSpeed = 1.5;
//水平方向視角限制
//controls.minAzimuthAngle = -Math.PI/4;
//controls.maxAzimuthAngle = Math.PI/4;
}
複製代碼
在3D模型中,鼠標點擊是重要的交互。對於 Three.js,它沒有相似 DOM 的層級關係,而且處於三維環境中,那麼咱們則須要經過如下方式來判斷某對象是否被選中。
function onDocumentMouseDown(event) {
// 點擊位置建立一個 THREE.Vector3 向量
var vector = new THREE.Vector3(( event.clientX / window.innerWidth ) * 2 - 1, -( event.clientY / window.innerHeight ) * 2 + 1, 0.5);
// vector.unproject 方法將屏幕上的點擊位置轉換成 Three.js 場景中的座標
vector = vector.unproject(camera);
// 使用 THREE.Raycaster 能夠向場景中發射光線
var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
// 使用 raycaster.intersectObjects 方法來判斷指定的對象中哪些被該光線照射到的,
// 從而顯示不一樣的顏色
var intersects = raycaster.intersectObjects([sphere, cylinder, cube]);
if (intersects.length > 0) {
console.log(intersects[0]);
// 點擊後改變透明度
intersects[0].object.material.transparent = true;
intersects[0].object.material.opacity = 0.1;
<!--...... 在這裏能夠實現你所須要的交互-->
}
}
複製代碼
// 引入相關的依賴
npm i -S three
<!--GisThree.js-->
<!--固然 這個代碼還有很大的優化空間啊!-->
import React, { Component, Fragment } from 'react';
import './GisThree.less';
import OBJLoader from './threejsLibs/OBJLoader';
import Orbitcontrols from './threejsLibs/OrbitControls';
import MTLLoader from './threejsLibs/MTLLoader_module';
import { Icon } from 'antd';
import exhibitObj from './modal/exhibit2.obj';
import exhibitMtl from './modal/exhibit2.mtl';
let THREE = require('three');
Orbitcontrols(THREE);
OBJLoader(THREE);
MTLLoader(THREE);
// 排除這些名字的3D模型
const objectArrName = [ "房屋1101", "房屋1150", "房屋600", "房屋70", "房屋45", "房屋362", "房屋363", "房屋364", "房屋500" ];
class GisThree extends Component {
constructor( props ) {
super(props);
this.state = {
isModel: false,
currentName: '暫無名字',
clientX: 0,
clientY: 0
};
this.threeRef = React.createRef();
}
componentDidMount() {
const width = window.innerWidth;
const height = window.innerHeight;
// todo 初始化場景
const scene = new THREE.Scene();
// todo 加載相機
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 80);
camera.position.set(0, 25, 25);
camera.lookAt(new THREE.Vector3(0, 0, 0));
//todo 加載光線
const ambLight = new THREE.AmbientLight(0x404040, 0.5);
const pointLight = new THREE.PointLight(0x404040, 0.8);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
pointLight.position.set(100, 10, 0);
pointLight.receiveShadow = true;
scene.add(ambLight);
scene.add(pointLight);
scene.add(directionalLight);
//todo renderer
const renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(width, height - 10);
//renderer.setClearColor(0xb9d3ff,1);
renderer.setClearColor(0x000000, 1.0);
//todo 加載模型model
let mtlLoader = new THREE.MTLLoader();
mtlLoader.load(exhibitMtl,
function ( materials ) {
console.log('sdj exhibit.obj', materials)
materials.preload();
let objLoader = new THREE.OBJLoader();
objLoader.setMaterials(materials);
objLoader.load(exhibitObj, function ( object ) {
console.log('sdj exhibit.obj')
console.log('sdj exhibit.obj object', object);
for ( let i = 0; i < object.children.length; i++ ) {
let material = object.children[ i ].material;
let meshObj = new THREE.Mesh(object.children[ i ].geometry, material);
meshObj.receiveShadow = true;
meshObj.castShadow = true;
meshObj.scale.set(0.02, 0.02, 0.02);
meshObj.name = "房屋" + i;
meshObj.position.x = 0;
meshObj.position.y = 0;
meshObj.position.z = -20;
scene.add(meshObj);
}
});
}
);
// todo 場景控制器初始化
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enabled = true; // 鼠標控制是否可用
// 是否自動旋轉
controls.autoRotate = true;
controls.autoRotateSpeed = 0.05;
//是否可旋轉,旋轉速度(鼠標左鍵)
controls.enableRotate = true;
controls.rotateSpeed = 0.3;
//controls.target = new THREE.Vector();//攝像機聚焦到某一個點
//最大最小相機移動距離(景深相機)
controls.minDistance = 10;
controls.maxDistance = 40;
//最大仰視角和俯視角
controls.minPolarAngle = Math.PI / 4; // 45度視角
controls.maxPolarAngle = Math.PI / 2.4; // 75度視角
//慣性滑動,滑動大小默認0.25
controls.enableDamping = true;
controls.dampingFactor = 0.25;
//是否可平移,默認移動速度爲7px
controls.enablePan = true;
controls.panSpeed = 0.5;
//controls.screenSpacePanning = true;
//滾輪縮放控制
controls.enableZoom = true;
controls.zoomSpeed = 1.5;
//水平方向視角限制
//controls.minAzimuthAngle = -Math.PI/4;
//controls.maxAzimuthAngle = Math.PI/4;
//todo 綁定到類上
this.scene = scene;
this.camera = camera;
this.renderer = renderer;
this.controls = controls;
//鼠標移入和移出事件高亮顯示選中的模型
this.currentObjectColor = null; //移入模型的顏色
this.currentObject = null; //鼠標移入的模型
// 初始化場景
// 加載到dom元素上
this.threeRef.current.appendChild(this.renderer.domElement)
this.start();
window.addEventListener('resize',this.resizeFunc1 ,false);
window.addEventListener('resize',this.resizeFunc2 ,false);
}
componentWillUnmount() {
this.stop();
this.threeRef.current.removeChild(this.renderer.domElement);
window.removeEventListener('resize',this.resizeFunc1 ,false);
window.removeEventListener('resize',this.resizeFunc2 ,false);
}
// 初始化
start = () => {
if(!this.frameId){
this.frameId = requestAnimationFrame(this.animate)
}
}
// 卸載組件的時候去除
stop = () => {
cancelAnimationFrame(this.frameId);
}
// 更新狀態
animate = () => {
this.controls.update();
this.renderScene();
this.frameId = requestAnimationFrame(this.animate);
}
renderScene = () => {
this.renderer.render(this.scene, this.camera);
}
// 是否展現彈窗
changeModel = ( e ) => {
e.stopPropagation();
this.setState({
isModel: !this.state.isModel
})
}
closeModel = ( e ) => {
e.stopPropagation();
if (this.controls && !this.controls.autoRotate){
this.controls.autoRotate = true;
}
this.setState({
isModel: false
})
}
// 點擊3D模型匹配
mouseClick = (e) => {
// 鼠標座標映射到三維座標
e.preventDefault();
const that = this;
const mouse = new THREE.Vector2();
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
if(!this.camera || !this.scene) return;
let vector = new THREE.Vector3(mouse.x, mouse.y, 0.5).unproject(this.camera);
let raycaster = new THREE.Raycaster(this.camera.position, vector.sub(this.camera.position).normalize());
let intersects = raycaster.intersectObjects(this.scene.children, true); //選中的三維模型
console.log('sdj position',intersects)
if (intersects.length > 0) {
let SELECTED = intersects[0];
let currentName = SELECTED.object.name;
console.log('sdj position', e.clientX, e.clientY, e.screenX, e.screenY);
if (objectArrName.indexOf(currentName) == -1) {
if (this.controls.autoRotate){
this.controls.autoRotate = false;
}
that.changeModel(e);
that.setState({
currentName,
clientX: e.clientX,
clientY: (e.clientY - 60)
})
console.log("你選中的物體的名字是:" + currentName);
}
}
}
// 鼠標聚焦
mouseenterObject = (e) => {
// 鼠標座標映射到三維座標
e.preventDefault();
let mouse = new THREE.Vector2();
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
let vector = new THREE.Vector3(mouse.x, mouse.y, 0.5).unproject(this.camera);
let raycaster = new THREE.Raycaster(this.camera.position, vector.sub(this.camera.position).normalize());
let intersects = raycaster.intersectObjects(this.scene.children, true); //選中的三維模型
if (!intersects.length && this.currentObjectColor && this.currentObject) { //從模型處移到外面
this.currentObject.object.material.color.setHex(this.currentObjectColor);
this.currentObjectColor = null;
this.currentObject = null;
}
if (intersects.length > 0) {
let SELECTED = intersects[0];
let currentName = SELECTED.object.name;
if (objectArrName.indexOf(currentName) == -1) {
if (this.currentObject && currentName === this.currentObject.object.name) {
return;
}
if (this.currentObjectColor && this.currentObject && currentName !== this.currentObject.object.name) { //color值是一個對象
this.currentObject.object.material.color.setHex(this.currentObjectColor);
}
this.currentObject = SELECTED;
this.currentObjectColor = SELECTED.object.material.color.getHex();
SELECTED.object.material.color.set(0x74bec1);
} else {
if (this.currentObjectColor && this.currentObject && currentName !== this.currentObject.object.name) { //color值是一個對象
this.currentObject.object.material.color.setHex(this.currentObjectColor);
}
this.currentObjectColor = null;
this.currentObject = null;
}
}
}
resizeFunc1 = () => {
this.controls.update();
}
resizeFunc2 = (e) => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
render() {
return (
<Fragment>
<div
className={ this.props.className || 'three-component' }
id="d3"
ref={ this.threeRef }
onClick={this.mouseClick}
onMouseMove={this.mouseenterObject}
/>
{
this.state.isModel && (
<div
className="three-modal"
style={ {
top: this.state.clientY,
left: this.state.clientX
} }
>
<Icon
className="three-modal-close"
type="close" theme="outlined"
onClick={ this.closeModel }
/>
<ul>
<li>
<span className="modal-title">出租屋編碼</span>
<span className="modal-data">{ this.state.currentName }</span>
</li>
<li>
<span className="modal-title">地址</span>
<span className="modal-data">社區一號</span>
</li>
<li>
<span className="modal-title">每層樓棟數</span>
<span className="modal-data">6</span>
</li>
<li>
<span className="modal-title">層數</span>
<span className="modal-data">16</span>
</li>
</ul>
</div>
)
}
</Fragment>
)
}
}
export default GisThree;
複製代碼
在服務器出現的錯誤,而本地服務器沒有問題 參考 stackoverflow.com/questions/4…
objLoader.js:624 Uncaught Error: THREE.OBJLoader: Unexpected line: "<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="theme-color" content="#000000"><link rel="manifest" href="/manifest.json"><link rel="shortcut icon" href="/favicon.ico"><title>智慧社區_管理後臺</title><link href="/static/css/main.bdb0e864.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="/config.js"></script><script type="text/javascript" src="/static/js/charts.24f90613.js"></script><script type="text/javascript" src="/static/js/vendor.0b9068d0.js"></script><script type="text/javascript" src="/static/js/main.cfa93993.js"></script></body></html>"
at OBJLoader.parse (objLoader.js:624)
at objLoader.js:385
at XMLHttpRequest.<anonymous> (three1.js:630)
objLoader.js:624 Uncaught Error: THREE.OBJLoader: Unexpected line: "<!doctype html>"
at OBJLoader.parse (objLoader.js:624)
at objLoader.js:385
at XMLHttpRequest.<anonymous> (three1.js:630)
複製代碼
最後發現棄用 mtl-loader 以後(且升級到 webpack4 )正確顯示了材質,以及出現了 git 忽略了 .obj 問題,看博客,全局的 gitignore_global.txt 中忽略了 .obj 問題,好坑!!!
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!