https://juejin.im/post/5ca22692f265da30a53d6656
github 的地址 欢迎 star!
之前项目中用到了 3D 模型演示的问题,整理了一下之前学习总结以及遇到的坑。3D 框架有老牌引擎 Three.js 和微软的 Babylon.js
主要来自于《Three.js 开发指南》也可以参考在线网站 threejs 教程
Sence 场景:场景是一个载体,容器,所有的一切都运行在这个容器里面(存放着所有渲染的物体和使用的光源)
相机 camera 的作用是定义可视域,相当于我们的双眼,生产一个个快照,最为常用的是 PerspectiveCamera 透视摄像机,其他还有 ArrayCamera 阵列摄像机(包含多个子摄像机,通过这一组子摄像机渲染出实际效果,适用于 VR 场景),CubeCamera 立方摄像机(创建六个 PerspectiveCamera(透视摄像机),适用于镜面场景),StereoCamera 立体相机(双透视摄像机适用于 3D 影片、视差效果)。相机主要分为两类正投影相机和透视相机,正投影相机的话, 所有方块渲染出来的尺寸都一样; 对象和相机之间的距离不会影响渲染结果,而透视相机接近真实世界,看物体会产生远近高低各不同
PerspectiveCamera 透视摄像机--模拟人眼的视觉,根据物体距离摄像机的距离,近大远小
// 场景是所有物体的容器
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 的基本概念
然后给出一个简单的例子
// 引入 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()
复制代码
在线的例子点击
主要是用来显示性能帧数的
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 问题,好坑!!!
如果有错误或者不严谨的地方,请务必给予指正,十分感谢!
原文:https://www.cnblogs.com/mazhenyu/p/11834700.html