在非使用unity作为3D渲染方案的前提下,对与目前web开发者比较友好的除了canvas场景需要的2D babylon.js
,fabric.js
, Three.js是目前针对于jsWeb用户最直接且比较友好的3D引擎方案了。
准备工作:
1.明确需要用的场景方案都有那些,模型需要的加载器是什么
2.模型的场景大小已经相关的交互业务
3.场景的工作环境(浏览器及硬件要求)
step1:
以.glb模型为例
import * as THREE from "three";
import {GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader";
import {OrbitControls} from "three/examples/jsm/controls/OrbitControls";
以上就是一个场景绘制需要的基本3个要素
模型压缩
由于建模工程师因为场景规模的原因模型在建立时过多了使用了面,导致整个模型的体积很大,一个校区或者大园区为例,建筑加环境要素及周边地形都整体的模型体积已经达到了100M+,这个时候就需要我们在开发前就考虑模型的压缩问题了
DRACO压缩算法
npm install -g gltf-pipeline
--input, -i Path to the glTF or glb file.[string] [required]
--output, -o Output path of the glTF or glb file. Separate
resources will be saved to the same directory.
[string]
--binary, -b Convert the input glTF to glb. //将输入的glTF转换为glb
[boolean] [default: false]
--json, -j Convert the input glb to glTF. //将输入的glb转换为glTF
[boolean] [default: false]
--separate, -s Write separate buffers, shaders, and textures //编写单独的缓冲区、着色器和纹理而不是把它们嵌入到glTF中
instead of embedding them in the glTF.
[boolean] [default: false]
--separateTextures, -t Write out separate textures only. //只写出单独的纹理
[boolean] [default: false]
--stats Print statistics to console for output glTF //将统计信息打印到控制台以输出glTF文件
file. [boolean] [default: false]
--keepUnusedElements Keep unused materials, nodes and meshes. //保留未使用的材质、节点和网格
[boolean] [default: false]
--draco.compressMeshes, -d Compress the meshes using Draco. Adds the //使用Draco压缩网格。添加KHR_draco_mesh_压缩扩展
KHR_draco_mesh_compression extension.
[boolean] [default: false]
--draco.compressionLevel Draco compression level [0-10], most is 10, //Draco压缩级别[0-10],大多数是10,最小值为0。值为0将会连续应用 编码并保留face顺序。
least is 0. A value of 0 will apply sequential
encoding and preserve face order.
[number] [default: 7]
--draco.quantizePositionBits Quantization bits for position attribute when //位置坐标属性的量化位使用Draco压缩。
using Draco compression. [number] [default: 11]
--draco.quantizeNormalBits Quantization bits for normal attribute when //法线属性的量化位使用Draco压缩
using Draco compression. [number] [default: 8]
--draco.quantizeTexcoordBits Quantization bits for texture coordinate //纹理坐标的量化位属性。
attribute when using Draco compression.
[number] [default: 10]
--draco.quantizeColorBits Quantization bits for color attribute when using //使用时颜色属性的量化位德拉科压缩
Draco compression. [number] [default: 8]
--draco.quantizeGenericBits Quantization bits for skinning attribute (joint //蒙皮属性(关节的量化位索引和关节权重)ad自定义属性使用Draco压缩时。
indices and joint weights) ad custom attributes
when using Draco compression. [number] [default: 8]
--draco.uncompressedFallback Adds uncompressed fallback versions of the //添加未压缩的回退版本压缩网格
compressed meshes. [boolean] [default: false]
--draco.unifiedQuantization Quantize positions of all primitives using the //统一定义的量化网格所有基本体的边界框。 如果这个选项未设置,对每个应用量化原始的可能会导致差距出现在不同图元之间。
same quantization grid defined by the unified
bounding box of all primitives. If this option
is not set, quantization is applied on each
primitive separately which can result in gaps
appearing between different primitives.
[boolean] [default: false]
gltf-pipeline的参数有很多这里我们只需要提炼出一个满足我们需要的就够了
gltf-pipeline -i .\public\tep\23.glb -o .\public\tep\23-main.glb -d --draco.compressionLevel 9 --draco.quantizePositionBits 10 --draco.quantizeColorBits 10
-i .\public\cascl\caa4.glb //输入路径
-o .\public\cascl\caa4-main.glb //输出路径及名称
-d --draco.compressionLevel 10 //压缩等级
--draco.quantizePositionBits 20 //量化 0 标识无损压缩
需要注意的是安装好之后是不可以直接运行的我们需要一个three.js为我们提供的基本依赖draco_decoder.js 这个文件一般就放在node_module/three/examples/js/libs/draco目录下cpoy出来与模型文件一起放在public文件夹下即可
完成上述这些准备工作之后我们开始渲染我们的第一个大园区场景,因为我们使用了压缩算法所以我们需要额外再引入一个解压加载器,并将我们copy出来的draco_decoder.js文件与我们压缩好的模型都放在public下
import {DRACOLoader} from "three/examples/jsm/loaders/DRACOLoader"
step2
初始一个加载模型的方法
export const initMod=(id,filePath,fun)=>{
container=document.getElementById(id);
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer({ antialias: true,alpha: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
renderer.setClearColor('#6fc0ec', 1.0);
renderer.outputEncoding = THREE.sRGBEncoding;
const loader = new GLTFLoader();
let dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("/cascl/"); // 设置public下的解码路径,注意最后面的/
dracoLoader.setDecoderConfig({ type: "js" });
dracoLoader.preload();
loader.setDRACOLoader(dracoLoader);
loader.load(
filePath,
gltf => {
// 将模型放到中间
const box = new THREE.Box3().setFromObject(gltf.scene);
const size = box.getSize(new THREE.Vector3()).length();
const center = box.getCenter(new THREE.Vector3());
gltf.scene.position.x -= center.x;
gltf.scene.position.y -= center.y;
gltf.scene.position.z -= center.z;
camera.near = size / 100;
camera.far = size * 100;
camera.updateProjectionMatrix();
camera.position.copy(center);
camera.position.x += size / 2;
camera.position.y += size / 2;
camera.position.z += size / 2;
camera.lookAt(center);
scene.add(gltf.scene);
console.log('---加载的模型',gltf.scene)
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);
//添加在模型的右上角高三倍设置一个光源 太阳
const light = new THREE.DirectionalLight(0xffffff, 1);
// 模型宽度
const width = box.max.x - box.min.x;
// 模型高度
const height = box.max.y - box.min.y;
// 模型深度
const depth = box.max.z - box.min.z;
light.position.set(width * 3, height * 3, depth * 3);
scene.add(light);
// 点光源
let point = new THREE.PointLight('#74beee',1);
point.position.set(-width * 3, -height * 3, depth * 3); // 点光源位置
scene.add(point); // 点光源添加到场景中
//多设置几个光源
const light3 = new THREE.DirectionalLight('#8dccee', 1);
light3.position.set(-width * 3, -height * 3, depth * 3);
scene.add(light3);
const light4 = new THREE.HemisphereLight('#8dccee', 0.3);
scene.add(light4);
//包含关键帧动画的模型作为参数创建一个播放器
mixer = new THREE.AnimationMixer(gltf.scene);
// 获取gltf.animations[0]的第一个clip动画对象
clipAction = mixer.clipAction(gltf.animations[0]); //创建动画clipAction对象
clipAction.play(); //播放动画
//不循环播放
clipAction.loop = THREE.LoopOnce;
// 物体状态停留在动画结束的时候
clipAction.clampWhenFinished = true
// 如果想播放动画,需要周期性执行`mixer.update()`更新AnimationMixer时间数据
clock = new THREE.Clock();
},
undefined,
error => {
console.error(error);
}
);
camera.position.z = 5;
// 添加OrbitControls控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 1;
controls.maxDistance = 1000;
const tag = labelTag({x: -580,y:50,z: -705});
tagList.push(tag)
scene.add(tag);//添加到指定的场景里
renderLabel()
animate();
runLoop()
renderer.domElement.addEventListener('click', handleModClick, false)
}
上方的初始方案中包含了一个基本的动画加载器由此我们可以完成一个基本的模型加载的场景创建
这是一个可以执行楼层分层爆炸的模型内置的动画由:
//包含关键帧动画的模型作为参数创建一个播放器
mixer = new THREE.AnimationMixer(gltf.scene);
完成捕捉及后续播放
step3
接下来我们完成园区的业务需求建设
1.场景需要有天空背景
2.场景需要有关键建筑的标注
3.场景的交互具有高亮环绕
4.场景具有漫游功能
基于以上我们开始增加需要的工具
- 场景漫游动画处理库
import TWEEN from '@tweenjs/tween.js';
2.场景天空环境即天空盒
const urls = [
'../sky/Above Day B_Cam_3_Right-X.png',//x正方形
'../sky/Above Day B_Cam_2_Left+X.png',//x负方向
'../sky/Above Day B_Cam_4_Up+Y.png',//y正方形
'../sky/Above Day B_Cam_5_Down-Y.png',//y负方向
'../sky/Above Day B_Cam_0_Front+Z.png',//z正方形
'../sky/Above Day B_Cam_1_Back-Z.png'//z负方形
]
const textureCube = new THREE.CubeTextureLoader().load(urls)
scene.background = textureCube
3.场景后处理器
import {EffectComposer} from 'three/examples/jsm/postprocessing/EffectComposer';
import {RenderPass} from 'three/examples/jsm/postprocessing/RenderPass';
import {OutlinePass} from 'three/examples/jsm/postprocessing/OutlinePass';
在使用了后处理器后因为模型抗拒齿原因我们需要在额外补充一个
import {SMAAPass} from 'three/examples/jsm/postprocessing/SMAAPass';
//抗锯齿后处理
const smaaPass = new SMAAPass(container.clientWidth * pixelRatio, container.clientHeight * pixelRatio);
4.一个漫游动画控制的方法
export const createCameraTween = (pos2, pos) => {
tween = new TWEEN.Tween({
// 相机开始坐标
x: camera.position.x,
y: camera.position.y,
z: camera.position.z,
// 相机开始指向的目标观察点
tx: current.x,
ty: current.y,
tz: current.z,
})
.to({
// 相机结束坐标
x: pos.x,
y: pos.y,
z: pos.z,
// 相机结束指向的目标观察点
tx: pos.x,
ty: pos.y,
tz: pos.z,
}, 2000)
.onUpdate(function (obj) {
// 动态改变相机位置
camera.position.set(obj.x, obj.y, obj.z);
// 动态计算相机视线
camera.lookAt(pos.x, pos.y, -pos.z);
})
.start();
animates();
}
const animates = (time) => {
TWEEN.update(time);
requestAnimationFrame(animates);
}
在使用glb/gltf模型中我们也常常会需要处理模型加载发暗,材质渲染失真的情况这里我们也一并加入到初始化的方案内
import {GammaCorrectionShader} from'three/examples/jsm/shaders/GammaCorrectionShader';
import {ShaderPass} from 'three/examples/jsm/postprocessing/ShaderPass';
import {RoomEnvironment} from 'three/examples/jsm/environments/RoomEnvironment';
由此我们以及基本完成了所有的加载需要的必备条件即要求,我们渲染一个大园区场景
场景后处理的效果业务由:
const renderOutline = (mod) => {
if (buildIds.includes(mod.name)) {
// 创建后处理对象EffectComposer,WebGL渲染器作为参数
composer = new EffectComposer(renderer);
renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 创建OutlinePass通道
container = document.getElementById('mod') ? document.getElementById('mod') : document.getElementById('mod2');
const v2 = new THREE.Vector2(container.clientWidth, container.clientHeight);
const outlinePass = new OutlinePass(v2, scene, camera);
// 创建伽马校正通道
const gammaPass = new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);
const pixelRatio = renderer.getPixelRatio()
//抗锯齿后处理
const smaaPass = new SMAAPass(container.clientWidth * pixelRatio, container.clientHeight * pixelRatio);
composer.addPass(smaaPass);
outlinePass.selectedObjects = [mod];
outlinePass.visibleEdgeColor.set('#a838ef');
outlinePass.edgeThickness = 4;
outlinePass.edgeStrength = 15;
outlinePass.pulsePeriod = 3;
composer.addPass(outlinePass);
animateOutline()
bus.$emit('showMod', mod)
} else {
clearOutline();
}
}
由于业务延展很多不再过的的赘述,解决方案包含了动态标签切换,标记交互,灯光动态,场景灯光随相机,标签随相机,场景模型过滤,场景模型设备等状态动态更新,单楼层模型,室内模型控制切换即各类物联网设备交互等等。